cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit e8ed1c43b60e0785be54b4841e678eac1d30cb08
parent 1293bd27719474b8f0e8a9aabc0ea79c92aea8c8
Author: triesap <tyson@radroots.org>
Date:   Wed, 15 Jul 2026 14:57:08 +0000

transport: align CLI with Reticulum transport identity

- replace preview and hybrid transport profile inputs with Reticulum and multi-target profiles
- separate radrootsd execution config from transport profile selection
- align SDK adapter, sync, mesh, and status views with Reticulum availability metadata
- remove stale proxy transport output and recovery hints

Diffstat:
MCargo.lock | 94++++++++++++++++++++++++++++++-------------------------------------------------
MCargo.toml | 2+-
Msrc/cli/input.rs | 21+++++----------------
Msrc/cli/mesh.rs | 4++--
Msrc/cli/mod.rs | 2+-
Msrc/cli/transport.rs | 34++++++++++++----------------------
Msrc/main.rs | 25+++++--------------------
Msrc/ops/error.rs | 16+++++-----------
Msrc/ops/exec/basket.rs | 14+++++++-------
Msrc/ops/exec/core.rs | 64+++++++---------------------------------------------------------
Msrc/ops/exec/farm.rs | 2+-
Msrc/ops/mod.rs | 2+-
Msrc/out/envelope.rs | 28++++++++++++++--------------
Msrc/out/terminal/actions.rs | 6+++---
Msrc/out/terminal/renderers/runtime.rs | 22++--------------------
Msrc/out/terminal/values.rs | 3+--
Msrc/registry/mesh.rs | 2+-
Msrc/runtime/config.rs | 326++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/runtime/farm.rs | 38++++++++++++++++++++++++++++++++++++--
Msrc/runtime/find.rs | 6+++---
Msrc/runtime/listing.rs | 25++++++++++++-------------
Msrc/runtime/mesh.rs | 14+++++++-------
Msrc/runtime/order.rs | 99++++++++++++++++++++++++++++++-------------------------------------------------
Msrc/runtime/order/sdk_status.rs | 21++++++++-------------
Msrc/runtime/provider.rs | 13++++---------
Msrc/runtime/runtime_store.rs | 13+++++++------
Msrc/runtime/sdk.rs | 224+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/runtime/signer.rs | 43+++----------------------------------------
Msrc/runtime/store.rs | 83+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/runtime/sync.rs | 311+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/runtime/transport.rs | 399+++++++++++++++++++++----------------------------------------------------------
Msrc/runtime/validation_receipt.rs | 57++++++---------------------------------------------------
Msrc/view/runtime.rs | 26+++++++++-----------------
Mtests/signer_runtime_modes.rs | 36++++++++++++++++++++++++++++++------
Mtests/support/mod.rs | 20++++++++++----------
Mtests/target_cli.rs | 1520++++++++-----------------------------------------------------------------------
36 files changed, 991 insertions(+), 2624 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1381,18 +1381,6 @@ dependencies = [ ] [[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] name = "fastrand" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1461,9 +1449,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ "futures-core", "futures-sink", @@ -1483,6 +1471,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1757,7 +1751,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1765,32 +1759,28 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.5", + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ "hashbrown 0.14.5", ] [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -3705,12 +3695,13 @@ dependencies = [ name = "radroots_geocoder" version = "0.1.0-alpha.2" dependencies = [ + "futures-executor", "hex", "radroots_runtime_paths", "reqwest", - "rusqlite", "serde", "sha2", + "sqlx", "tempfile", "thiserror 1.0.69", "url", @@ -3973,9 +3964,10 @@ name = "radroots_sql_core" version = "0.1.0-alpha.2" dependencies = [ "chrono", - "rusqlite", + "futures-executor", "serde", "serde_json", + "sqlx", "uuid", ] @@ -4312,20 +4304,6 @@ dependencies = [ ] [[package]] -name = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink 0.9.1", - "libsqlite3-sys", - "smallvec", -] - -[[package]] name = "rust-ini" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5834,9 +5812,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ "sqlx-core", "sqlx-macros", @@ -5845,12 +5823,13 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ "base64 0.22.1", "bytes", + "cfg-if", "crc", "crossbeam-queue", "either", @@ -5859,12 +5838,11 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", - "hashlink 0.10.0", + "hashbrown 0.16.1", + "hashlink 0.11.1", "indexmap 2.13.1", "log", "memchr", - "once_cell", "percent-encoding", "serde", "sha2", @@ -5878,9 +5856,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", @@ -5891,15 +5869,15 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" dependencies = [ + "cfg-if", "dotenvy", "either", "heck", "hex", - "once_cell", "proc-macro2", "quote", "serde", @@ -5914,12 +5892,13 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", "flume", + "form_urlencoded", "futures-channel", "futures-core", "futures-executor", @@ -5929,7 +5908,6 @@ dependencies = [ "log", "percent-encoding", "serde", - "serde_urlencoded", "sqlx-core", "thiserror 2.0.18", "tracing", diff --git a/Cargo.toml b/Cargo.toml @@ -49,7 +49,7 @@ radroots_transport_nostr = { path = "../lib/crates/transport_nostr", default-fea ] } radroots_runtime = { path = "../lib/crates/runtime" } radroots_runtime_paths = { path = "../lib/crates/runtime_paths" } -radroots_sdk = { path = "../sdk/crates/sdk", features = ["local-runtime-radrootsd-proxy", "transport-nostr-runtime"] } +radroots_sdk = { path = "../sdk/crates/sdk", features = ["local-runtime-radrootsd-execution", "transport-nostr-runtime"] } radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] } radroots_sql_core = { path = "../lib/crates/sql_core", features = ["native"] } radroots_trade_sp1_host = { path = "../lib/crates/trade_sp1_host" } diff --git a/src/cli/input.rs b/src/cli/input.rs @@ -331,28 +331,17 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData { Value::String(args.kind.as_str().to_owned()), ); insert_string_array(&mut input, "nostr_relays", args.nostr_relay.as_slice()); - if let Some(behavior) = args.reticulum_preview_behavior { + if let Some(behavior) = args.reticulum_behavior { input.insert( - "reticulum_preview_behavior".to_owned(), + "reticulum_behavior".to_owned(), Value::String(behavior.as_str().to_owned()), ); } + insert_string(&mut input, "reticulum_scope", &args.reticulum_scope); insert_string( &mut input, - "reticulum_preview_scope", - &args.reticulum_preview_scope, - ); - insert_string( - &mut input, - "reticulum_preview_agent_endpoint", - &args.reticulum_preview_agent_endpoint, - ); - insert_string(&mut input, "proxy_url", &args.proxy_url); - insert_path(&mut input, "proxy_token_file", &args.proxy_token_file); - insert_string( - &mut input, - "proxy_token_secret_id", - &args.proxy_token_secret_id, + "reticulum_agent_endpoint", + &args.reticulum_agent_endpoint, ); } TransportProfileCommand::Get => {} diff --git a/src/cli/mesh.rs b/src/cli/mesh.rs @@ -34,14 +34,14 @@ pub struct MeshScopeSetArgs { #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum MeshScopeArg { Disabled, - LocalPreview, + Local, } impl MeshScopeArg { pub fn as_str(self) -> &'static str { match self { Self::Disabled => "disabled", - Self::LocalPreview => "local_preview", + Self::Local => "local", } } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs @@ -92,7 +92,7 @@ pub enum TargetCommand { Signer(SignerArgs), #[command(about = "Manage transport profiles and outbox delivery.")] Transport(TransportArgs), - #[command(about = "Inspect mesh scope and Reticulum preview policy.")] + #[command(about = "Inspect mesh scope and Reticulum admission policy.")] Mesh(MeshArgs), #[command(about = "Initialize and inspect the local replica store.")] Store(StoreArgs), diff --git a/src/cli/transport.rs b/src/cli/transport.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use clap::{Args, Subcommand, ValueEnum}; #[derive(Debug, Clone, Args)] @@ -33,27 +31,20 @@ pub struct TransportProfileSetArgs { pub kind: TransportProfileKindArg, #[arg(long = "nostr-relay")] pub nostr_relay: Vec<String>, - #[arg(long = "reticulum-preview-behavior", value_enum)] - pub reticulum_preview_behavior: Option<ReticulumPreviewBehaviorArg>, - #[arg(long = "reticulum-preview-scope")] - pub reticulum_preview_scope: Option<String>, - #[arg(long = "reticulum-preview-agent-endpoint")] - pub reticulum_preview_agent_endpoint: Option<String>, - #[arg(long = "proxy-url")] - pub proxy_url: Option<String>, - #[arg(long = "proxy-token-file", value_name = "PATH")] - pub proxy_token_file: Option<PathBuf>, - #[arg(long = "proxy-token-secret-id", value_name = "SECRET_ID")] - pub proxy_token_secret_id: Option<String>, + #[arg(long = "reticulum-behavior", value_enum)] + pub reticulum_behavior: Option<ReticulumBehaviorArg>, + #[arg(long = "reticulum-scope")] + pub reticulum_scope: Option<String>, + #[arg(long = "reticulum-agent-endpoint")] + pub reticulum_agent_endpoint: Option<String>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum TransportProfileKindArg { LocalOnly, Nostr, - ReticulumPreview, - Hybrid, - Proxy, + Reticulum, + MultiTarget, } impl TransportProfileKindArg { @@ -61,20 +52,19 @@ impl TransportProfileKindArg { match self { Self::LocalOnly => "local_only", Self::Nostr => "nostr", - Self::ReticulumPreview => "reticulum_preview", - Self::Hybrid => "hybrid", - Self::Proxy => "proxy", + Self::Reticulum => "reticulum", + Self::MultiTarget => "multi_target", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum ReticulumPreviewBehaviorArg { +pub enum ReticulumBehaviorArg { RejectDeliveryAttempts, DeferDeliveryPlans, } -impl ReticulumPreviewBehaviorArg { +impl ReticulumBehaviorArg { pub fn as_str(self) -> &'static str { match self { Self::RejectDeliveryAttempts => "reject_delivery_attempts", diff --git a/src/main.rs b/src/main.rs @@ -504,7 +504,7 @@ fn validate_transport_profile_contract( } if matches!( config.transport.profile, - TransportProfileKind::LocalOnly | TransportProfileKind::ReticulumPreview + TransportProfileKind::LocalOnly | TransportProfileKind::Reticulum ) { return Err(OperationAdapterError::NetworkUnavailable { operation_id: spec.operation_id.to_owned(), @@ -517,7 +517,7 @@ fn validate_transport_profile_contract( } if matches!( config.transport.profile, - TransportProfileKind::Nostr | TransportProfileKind::Hybrid + TransportProfileKind::Nostr | TransportProfileKind::MultiTarget ) && config.transport.nostr_relay_urls.is_empty() { return Err(OperationAdapterError::NetworkUnavailable { @@ -528,23 +528,12 @@ fn validate_transport_profile_contract( ), }); } - if matches!(config.transport.profile, TransportProfileKind::Proxy) - && let Err(error) = runtime::transport::proxy_token_ready(config) - { - return Err(OperationAdapterError::NetworkUnavailable { - operation_id: spec.operation_id.to_owned(), - message: format!( - "`{}` requires a usable proxy token source: {error}", - spec.cli_path, - ), - }); - } Ok(()) } fn transport_profile_delivery_unavailable_reason(config: &RuntimeConfig) -> Option<String> { match config.transport.profile { - TransportProfileKind::Nostr | TransportProfileKind::Hybrid => { + TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => { config.transport.nostr_relay_urls.is_empty().then(|| { format!( "active {} transport profile has no configured Nostr relay targets", @@ -552,15 +541,11 @@ fn transport_profile_delivery_unavailable_reason(config: &RuntimeConfig) -> Opti ) }) } - TransportProfileKind::Proxy => match runtime::transport::proxy_token_ready(config) { - Ok(()) => None, - Err(error) => Some(error.to_string()), - }, TransportProfileKind::LocalOnly => { Some("active local_only transport profile cannot deliver".to_owned()) } - TransportProfileKind::ReticulumPreview => { - Some("active reticulum_preview transport profile cannot deliver".to_owned()) + TransportProfileKind::Reticulum => { + Some("active reticulum transport profile cannot deliver".to_owned()) } } } diff --git a/src/ops/error.rs b/src/ops/error.rs @@ -552,7 +552,6 @@ fn sdk_error_exit_code(class: RadrootsSdkErrorClass) -> CliExitCode { RadrootsSdkErrorClass::Clock | RadrootsSdkErrorClass::Configuration | RadrootsSdkErrorClass::Request => CliExitCode::InvalidInput, - RadrootsSdkErrorClass::LocalMutation => CliExitCode::Conflict, RadrootsSdkErrorClass::Storage => CliExitCode::RuntimeUnavailable, RadrootsSdkErrorClass::Transport => CliExitCode::SyncOrNetworkFailure, RadrootsSdkErrorClass::Unsupported => CliExitCode::RuntimeUnavailable, @@ -565,7 +564,6 @@ fn sdk_error_class_name(class: RadrootsSdkErrorClass) -> &'static str { RadrootsSdkErrorClass::Authorization => "authorization", RadrootsSdkErrorClass::Clock => "clock", RadrootsSdkErrorClass::Configuration => "configuration", - RadrootsSdkErrorClass::LocalMutation => "local_mutation", RadrootsSdkErrorClass::Request => "request", RadrootsSdkErrorClass::Storage => "storage", RadrootsSdkErrorClass::Transport => "transport", @@ -581,8 +579,7 @@ fn sdk_recovery_next_actions( recovery_actions .iter() .filter_map(|action| match action { - RadrootsSdkRecoveryAction::RetryOutboxEnqueue - | RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey + RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey | RadrootsSdkRecoveryAction::FixRequest => Some(operation_retry_action(operation_id)), RadrootsSdkRecoveryAction::InspectLocalStores => { Some("radroots store status get".to_owned()) @@ -772,10 +769,7 @@ fn classify_runtime_failure( }, }; } - if contains_any( - &lowered, - &["provider", "write-plane", "write plane", "rpc", "proxy"], - ) { + if contains_any(&lowered, &["provider", "write-plane", "write plane", "rpc"]) { return match availability { RuntimeFailureAvailability::Unconfigured => { OperationAdapterError::ProviderUnconfigured { @@ -804,7 +798,7 @@ fn looks_like_auth_failure(value: &str) -> bool { value, &[ "authentication", - "daemon proxy auth", + "daemon execution auth", "authorization", "authorize", "unauthorized", @@ -843,7 +837,7 @@ fn looks_like_provider_failure(value: &str) -> bool { "provider unconfigured", "provider runtime", "provider failed", - "proxy provider", + "execution provider", ], ) } @@ -858,7 +852,7 @@ fn looks_like_operation_failure(value: &str) -> bool { "unsupported operation", "operation unavailable", "operation disabled", - "publish proxy disabled", + "publish execution disabled", "publish.event is disabled", ], ) diff --git a/src/ops/exec/basket.rs b/src/ops/exec/basket.rs @@ -6,7 +6,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use radroots_event::order::RadrootsOrderEconomics; use radroots_replica_schema::trade_product::{ITradeProductFieldsFilter, ITradeProductFindMany}; use radroots_replica_store::{ReplicaSql, trade_product}; -use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::SqlxSqliteExecutor; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -880,7 +880,7 @@ fn basket_market_issues( "current local replica data is required before quote creation; run `radroots store init` and `radroots market refresh`", )]); } - let executor = SqliteExecutor::open(&config.local.replica_store_path).map_err(|error| { + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path).map_err(|error| { OperationAdapterError::Runtime(format!( "open local replica {}: {error}", config.local.replica_store_path.display() @@ -955,7 +955,7 @@ fn basket_market_issues( fn basket_product_bin_state( config: &RuntimeConfig, - executor: &SqliteExecutor, + executor: &SqlxSqliteExecutor, item: &BasketItem, ) -> Result<BasketProductResolution, OperationAdapterError> { if let Some(listing_addr) = item.listing_addr.as_deref().and_then(non_empty_ref) { @@ -984,7 +984,7 @@ fn basket_product_bin_state( return Ok(BasketProductResolution::Unresolved); }; let lookup_executor = - SqliteExecutor::open(&config.local.replica_store_path).map_err(|error| { + SqlxSqliteExecutor::open(&config.local.replica_store_path).map_err(|error| { OperationAdapterError::Runtime(format!( "open local replica {}: {error}", config.local.replica_store_path.display() @@ -1320,7 +1320,7 @@ mod tests { use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event}; use radroots_secret_vault::RadrootsSecretBackend; - use radroots_sql_core::{SqlExecutor, SqliteExecutor}; + use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor}; use serde_json::{Map, Value, json}; use tempfile::tempdir; @@ -1821,7 +1821,7 @@ mod tests { }) .expect("seed listing event"); let executor = - SqliteExecutor::open(&config.local.replica_store_path).expect("open replica"); + SqlxSqliteExecutor::open(&config.local.replica_store_path).expect("open replica"); assert_eq!( radroots_replica_ingest_event(&executor, &event).expect("ingest listing"), RadrootsReplicaIngestOutcome::Applied @@ -1837,7 +1837,7 @@ mod tests { fn duplicate_current_listing_row(config: &RuntimeConfig) { let executor = - SqliteExecutor::open(&config.local.replica_store_path).expect("open replica"); + SqlxSqliteExecutor::open(&config.local.replica_store_path).expect("open replica"); let params = json!(["33333333-3333-3333-3333-333333333333", LISTING_ADDR]).to_string(); executor .exec( diff --git a/src/ops/exec/core.rs b/src/ops/exec/core.rs @@ -804,7 +804,7 @@ fn publish_runtime_view( }; match config.transport.profile { - TransportProfileKind::Nostr | TransportProfileKind::Hybrid => { + TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => { let (state, executable, reason) = nostr_publish_readiness(config, relay_ready, signed_write_required, account); PublishRuntimeView { @@ -824,25 +824,6 @@ fn publish_runtime_view( }, } } - TransportProfileKind::Proxy => { - let (state, executable, reason) = proxy_publish_readiness(config); - PublishRuntimeView { - transport: config.transport.profile.as_str().to_owned(), - source, - transport_family: config.transport.profile.transport_family().to_owned(), - state: state.to_owned(), - executable, - reason: reason.clone(), - signed_write_required, - relay, - provider: PublishProviderRuntimeView { - provider_runtime_id: "proxy".to_owned(), - state: state.to_owned(), - source: "publish transport · transport profile".to_owned(), - reason, - }, - } - } TransportProfileKind::LocalOnly => PublishRuntimeView { transport: config.transport.profile.as_str().to_owned(), source, @@ -861,18 +842,18 @@ fn publish_runtime_view( reason: Some("local_only transport profile writes only to local state".to_owned()), }, }, - TransportProfileKind::ReticulumPreview => PublishRuntimeView { + TransportProfileKind::Reticulum => PublishRuntimeView { transport: config.transport.profile.as_str().to_owned(), source, transport_family: config.transport.profile.transport_family().to_owned(), - state: "preview_unavailable".to_owned(), + state: "unavailable".to_owned(), executable: false, reason: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()), signed_write_required, relay, provider: PublishProviderRuntimeView { - provider_runtime_id: "reticulum_preview".to_owned(), - state: "preview_unavailable".to_owned(), + provider_runtime_id: "reticulum".to_owned(), + state: "unavailable".to_owned(), source: config.transport.source.as_str().to_owned(), reason: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()), }, @@ -934,23 +915,6 @@ fn nostr_publish_readiness( ("ready", true, None) } -fn proxy_publish_readiness(config: &RuntimeConfig) -> (&'static str, bool, Option<String>) { - if let Err(error) = crate::runtime::transport::proxy_token_ready(config) { - return ("unconfigured", false, Some(error.to_string())); - } - - if matches!(config.signer.backend, SignerBackend::Myc) { - let signer = resolve_signer_status(config); - return if signer.state == "ready" { - ("ready", true, None) - } else { - ("unconfigured", false, signer.reason) - }; - } - - ("ready", true, None) -} - fn signer_health_view(config: &RuntimeConfig, account: &AccountResolution) -> Value { match config.signer.backend { SignerBackend::Local => { @@ -1101,7 +1065,7 @@ fn publish_recovery_actions( let mut actions = Vec::new(); match config.transport.profile { - TransportProfileKind::Nostr | TransportProfileKind::Hybrid => { + TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => { if config.transport.nostr_relay_urls.is_empty() { push_unique( &mut actions, @@ -1120,27 +1084,13 @@ fn publish_recovery_actions( } } } - TransportProfileKind::Proxy => { - if crate::runtime::transport::proxy_token_ready(config).is_ok() { - if publish.signed_write_required - && matches!(config.signer.backend, SignerBackend::Myc) - { - push_unique(&mut actions, "radroots signer status get"); - } - } else { - push_unique( - &mut actions, - "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID", - ); - } - } TransportProfileKind::LocalOnly => { push_unique( &mut actions, "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com", ); } - TransportProfileKind::ReticulumPreview => { + TransportProfileKind::Reticulum => { push_unique( &mut actions, "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com", diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs @@ -234,7 +234,7 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> { } if matches!( self.config.transport.profile, - TransportProfileKind::Nostr | TransportProfileKind::Hybrid + TransportProfileKind::Nostr | TransportProfileKind::MultiTarget ) { require_nostr_delivery_target(&request, self.config)?; } diff --git a/src/ops/mod.rs b/src/ops/mod.rs @@ -598,7 +598,7 @@ mod tests { ( OperationAdapterError::unavailable( "listing.publish", - "proxy provider is unavailable".to_owned(), + "transport provider is unavailable".to_owned(), ), "provider_unavailable", "provider", diff --git a/src/out/envelope.rs b/src/out/envelope.rs @@ -276,26 +276,26 @@ fn next_actions_from_actions_value(actions_value: Option<&Value>) -> Vec<NextAct fn next_action_from_action_string(action: &str) -> Option<NextAction> { let action = action.trim(); - if action - == "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID" - { + if action == "configure signer.remote_nip46 signer_session_ref" { return Some(NextAction { kind: NextActionKind::OperatorConfig, - label: "configure proxy token source".to_owned(), + label: "configure signer session binding".to_owned(), command: None, description: Some(action.to_owned()), - env_var: Some("RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE".to_owned()), - config_key: None, + env_var: None, + config_key: Some("signer.remote_nip46.signer_session_ref".to_owned()), }); } - if action == "configure signer.remote_nip46 signer_session_ref" { + if action + == "configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID" + { return Some(NextAction { kind: NextActionKind::OperatorConfig, - label: "configure signer session binding".to_owned(), + label: "configure radrootsd execution token source".to_owned(), command: None, description: Some(action.to_owned()), - env_var: None, - config_key: Some("signer.remote_nip46.signer_session_ref".to_owned()), + env_var: Some("RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE".to_owned()), + config_key: None, }); } let command = action.trim().strip_prefix("run ").unwrap_or(action).trim(); @@ -606,9 +606,9 @@ mod tests { ); error.detail = Some(json!({ "actions": [ - "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID", + "configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID", "configure signer.remote_nip46 signer_session_ref", - "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID" + "configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID" ] })); let envelope = OutputEnvelope::failure( @@ -625,12 +625,12 @@ mod tests { ); assert_eq!( envelope.next_actions[0].label, - "configure proxy token source" + "configure radrootsd execution token source" ); assert_eq!(envelope.next_actions[0].command, None); assert_eq!( envelope.next_actions[0].env_var.as_deref(), - Some("RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE") + Some("RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE") ); assert_eq!( envelope.next_actions[1].kind, diff --git a/src/out/terminal/actions.rs b/src/out/terminal/actions.rs @@ -316,8 +316,8 @@ mod tests { kind: NextActionKind::OperatorConfig, label: "configure token".to_owned(), command: None, - description: Some("configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE".to_owned()), - env_var: Some("RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE".to_owned()), + description: Some("configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE".to_owned()), + env_var: Some("RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE".to_owned()), config_key: None, }; @@ -327,7 +327,7 @@ mod tests { assert_eq!(terminal[0].kind, TerminalActionKind::Setup); assert_eq!( terminal[0].description.as_deref(), - Some("configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE") + Some("configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE") ); } } diff --git a/src/out/terminal/renderers/runtime.rs b/src/out/terminal/renderers/runtime.rs @@ -107,24 +107,6 @@ fn transport_profile_document(envelope: &OutputEnvelope, result: &Value) -> Term &["profile_delivery_usable"], ); common::push_path_field(&mut document, "Message", result, &["message"]); - common::push_path_field( - &mut document, - "Proxy token", - result, - &["proxy_token_source"], - ); - common::push_path_field( - &mut document, - "Proxy token file", - result, - &["proxy_token_file"], - ); - common::push_path_field( - &mut document, - "Proxy token secret", - result, - &["proxy_token_secret_id"], - ); if let Some(relays) = common::array(result, &["nostr_relays"]) { let rows = relays .iter() @@ -233,9 +215,9 @@ fn transport_outbox_status_document(envelope: &OutputEnvelope, result: &Value) - common::push_count_field(&mut document, "Terminal", result, &["terminal_count"]); common::push_count_field( &mut document, - "Preview unavailable", + "Deferred", result, - &["preview_unavailable_count"], + &["deferred_until_implemented_count"], ); common::push_count_field( &mut document, diff --git a/src/out/terminal/values.rs b/src/out/terminal/values.rs @@ -5,9 +5,8 @@ use serde_json::Value; pub fn transport_label(value: &str) -> String { match value { "nostr" => "nostr", - "proxy" => "proxy", + "reticulum" => "reticulum", "local" => "local", - "preview" => "preview", other => other, } .to_owned() diff --git a/src/registry/mesh.rs b/src/registry/mesh.rs @@ -55,7 +55,7 @@ pub const MESH_POLICY_CHECK: OperationSpec = operation!( "mesh_policy_check", "MeshPolicyCheckRequest", "MeshPolicyCheckResult", - "Evaluate mesh delivery policy for the active preview state.", + "Evaluate mesh delivery policy for the active Reticulum availability state.", Any, false, None, diff --git a/src/runtime/config.rs b/src/runtime/config.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use radroots_runtime::{parse_bool_value, parse_strict_env_file, parse_u64_value}; use radroots_runtime_paths::RadrootsPathResolver; use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend}; -use radroots_transport::{RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, RadrootsTransportMeshScopeId}; +use radroots_transport::{RADROOTS_RETICULUM_SCOPE_ID, RadrootsTransportMeshScopeId}; use radroots_transport_nostr::{ RadrootsRelayTransportError, RadrootsRelayUrl, RadrootsRelayUrlPolicy, }; @@ -44,16 +44,14 @@ const ENV_CLI_IDENTITY_PATH: &str = "RADROOTS_CLI_IDENTITY_PATH"; const ENV_CLI_SIGNER_BACKEND: &str = "RADROOTS_CLI_SIGNER_BACKEND"; const ENV_CLI_TRANSPORT_PROFILE: &str = "RADROOTS_CLI_TRANSPORT_PROFILE"; const ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS: &str = "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS"; -const ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR: &str = - "RADROOTS_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR"; -const ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_SCOPE: &str = - "RADROOTS_CLI_TRANSPORT_RETICULUM_PREVIEW_SCOPE"; -const ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_AGENT_ENDPOINT: &str = - "RADROOTS_CLI_TRANSPORT_RETICULUM_PREVIEW_AGENT_ENDPOINT"; -const ENV_CLI_TRANSPORT_PROXY_URL: &str = "RADROOTS_CLI_TRANSPORT_PROXY_URL"; -const ENV_CLI_TRANSPORT_PROXY_TOKEN_FILE: &str = "RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE"; -const ENV_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID: &str = - "RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"; +const ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR: &str = "RADROOTS_CLI_TRANSPORT_RETICULUM_BEHAVIOR"; +const ENV_CLI_TRANSPORT_RETICULUM_SCOPE: &str = "RADROOTS_CLI_TRANSPORT_RETICULUM_SCOPE"; +const ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT: &str = + "RADROOTS_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT"; +const ENV_CLI_RADROOTSD_EXECUTION_URL: &str = "RADROOTS_CLI_RADROOTSD_EXECUTION_URL"; +const ENV_CLI_RADROOTSD_EXECUTION_TOKEN_FILE: &str = "RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE"; +const ENV_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID: &str = + "RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID"; const ENV_CLI_MYC_EXECUTABLE: &str = "RADROOTS_CLI_MYC_EXECUTABLE"; const ENV_CLI_MYC_STATUS_TIMEOUT_MS: &str = "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS"; const ENV_CLI_HYF_ENABLED: &str = "RADROOTS_CLI_HYF_ENABLED"; @@ -73,12 +71,12 @@ const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[ ENV_CLI_SIGNER_BACKEND, ENV_CLI_TRANSPORT_PROFILE, ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS, - ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR, - ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_SCOPE, - ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_AGENT_ENDPOINT, - ENV_CLI_TRANSPORT_PROXY_URL, - ENV_CLI_TRANSPORT_PROXY_TOKEN_FILE, - ENV_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID, + ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR, + ENV_CLI_TRANSPORT_RETICULUM_SCOPE, + ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT, + ENV_CLI_RADROOTSD_EXECUTION_URL, + ENV_CLI_RADROOTSD_EXECUTION_TOKEN_FILE, + ENV_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID, ENV_CLI_MYC_EXECUTABLE, ENV_CLI_MYC_STATUS_TIMEOUT_MS, ENV_CLI_HYF_ENABLED, @@ -192,9 +190,8 @@ pub struct SignerConfig { pub enum TransportProfileKind { LocalOnly, Nostr, - ReticulumPreview, - Hybrid, - Proxy, + Reticulum, + MultiTarget, } impl TransportProfileKind { @@ -202,9 +199,8 @@ impl TransportProfileKind { match self { Self::LocalOnly => "local_only", Self::Nostr => "nostr", - Self::ReticulumPreview => "reticulum_preview", - Self::Hybrid => "hybrid", - Self::Proxy => "proxy", + Self::Reticulum => "reticulum", + Self::MultiTarget => "multi_target", } } @@ -212,20 +208,19 @@ impl TransportProfileKind { match self { Self::LocalOnly => "local", Self::Nostr => "nostr", - Self::ReticulumPreview => "reticulum", - Self::Hybrid => "hybrid", - Self::Proxy => "proxy", + Self::Reticulum => "reticulum", + Self::MultiTarget => "multi_target", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ReticulumPreviewBehavior { +pub enum ReticulumBehavior { RejectDeliveryAttempts, DeferDeliveryPlans, } -impl ReticulumPreviewBehavior { +impl ReticulumBehavior { pub fn as_str(self) -> &'static str { match self { Self::RejectDeliveryAttempts => "reject_delivery_attempts", @@ -258,10 +253,10 @@ pub struct TransportConfig { pub profile: TransportProfileKind, pub source: TransportConfigSource, pub nostr_relay_urls: Vec<String>, - pub reticulum_preview_behavior: ReticulumPreviewBehavior, - pub reticulum_preview_scope: String, - pub reticulum_preview_agent_endpoint: Option<String>, - pub proxy: ProxyTransportConfig, + pub reticulum_behavior: ReticulumBehavior, + pub reticulum_scope: String, + pub reticulum_agent_endpoint: Option<String>, + pub radrootsd_execution: RadrootsdExecutionConfig, } impl TransportConfig { @@ -270,10 +265,10 @@ impl TransportConfig { profile: TransportProfileKind::LocalOnly, source: TransportConfigSource::Defaults, nostr_relay_urls: Vec::new(), - reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts, - reticulum_preview_scope: RADROOTS_RETICULUM_PREVIEW_SCOPE_ID.to_owned(), - reticulum_preview_agent_endpoint: None, - proxy: ProxyTransportConfig::default(), + reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts, + reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(), + reticulum_agent_endpoint: None, + radrootsd_execution: RadrootsdExecutionConfig::default(), } } @@ -285,22 +280,22 @@ impl TransportConfig { profile: TransportProfileKind::Nostr, source: TransportConfigSource::Defaults, nostr_relay_urls, - reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts, - reticulum_preview_scope: RADROOTS_RETICULUM_PREVIEW_SCOPE_ID.to_owned(), - reticulum_preview_agent_endpoint: None, - proxy: ProxyTransportConfig::default(), + reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts, + reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(), + reticulum_agent_endpoint: None, + radrootsd_execution: RadrootsdExecutionConfig::default(), } } } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProxyTransportConfig { +pub struct RadrootsdExecutionConfig { pub url: String, pub token_file: Option<PathBuf>, pub token_secret_id: Option<String>, } -impl Default for ProxyTransportConfig { +impl Default for RadrootsdExecutionConfig { fn default() -> Self { Self { url: DEFAULT_RPC_URL.to_owned(), @@ -333,14 +328,14 @@ pub struct HyfConfig { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MeshScope { Disabled, - LocalPreview, + Local, } impl MeshScope { pub fn as_str(self) -> &'static str { match self { Self::Disabled => "disabled", - Self::LocalPreview => "local_preview", + Self::Local => "local", } } } @@ -537,8 +532,8 @@ struct IdentityFileConfig { struct TransportFileConfig { profile: Option<String>, nostr: Option<NostrTransportFileConfig>, - reticulum_preview: Option<ReticulumPreviewFileConfig>, - proxy: Option<TransportProxyFileConfig>, + reticulum: Option<ReticulumFileConfig>, + radrootsd_execution: Option<RadrootsdExecutionFileConfig>, } #[derive(Debug, Default, Deserialize)] @@ -549,7 +544,7 @@ struct NostrTransportFileConfig { #[derive(Debug, Default, Deserialize)] #[serde(default, deny_unknown_fields)] -struct ReticulumPreviewFileConfig { +struct ReticulumFileConfig { behavior: Option<String>, scope: Option<String>, agent_endpoint: Option<String>, @@ -557,7 +552,7 @@ struct ReticulumPreviewFileConfig { #[derive(Debug, Default, Deserialize)] #[serde(default, deny_unknown_fields)] -struct TransportProxyFileConfig { +struct RadrootsdExecutionFileConfig { url: Option<String>, token_file: Option<PathBuf>, token_secret_id: Option<String>, @@ -988,41 +983,25 @@ fn resolve_transport_config( ) -> Result<TransportConfig, RuntimeError> { if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_TRANSPORT_PROFILE]) { let profile = parse_transport_profile_kind(key.as_str(), value.as_str())?; - let reticulum_preview_behavior = env_value( - env, - env_file, - &[ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR], - ) - .map(|value| { - parse_reticulum_preview_behavior( - ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_BEHAVIOR, - value.as_str(), - ) - }) - .transpose()? - .unwrap_or(ReticulumPreviewBehavior::RejectDeliveryAttempts); - let reticulum_preview_scope = - env_value(env, env_file, &[ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_SCOPE]) + let reticulum_behavior = env_value(env, env_file, &[ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR]) + .map(|value| { + parse_reticulum_behavior(ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR, value.as_str()) + }) + .transpose()? + .unwrap_or(ReticulumBehavior::RejectDeliveryAttempts); + let reticulum_scope = env_value(env, env_file, &[ENV_CLI_TRANSPORT_RETICULUM_SCOPE]) + .map(|value| parse_reticulum_scope(ENV_CLI_TRANSPORT_RETICULUM_SCOPE, value.as_str())) + .transpose()? + .unwrap_or_else(|| RADROOTS_RETICULUM_SCOPE_ID.to_owned()); + let reticulum_agent_endpoint = + env_value(env, env_file, &[ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT]) .map(|value| { - parse_reticulum_preview_scope( - ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_SCOPE, + parse_reticulum_agent_endpoint( + ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT, value.as_str(), ) }) - .transpose()? - .unwrap_or_else(|| RADROOTS_RETICULUM_PREVIEW_SCOPE_ID.to_owned()); - let reticulum_preview_agent_endpoint = env_value( - env, - env_file, - &[ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_AGENT_ENDPOINT], - ) - .map(|value| { - parse_reticulum_preview_agent_endpoint( - ENV_CLI_TRANSPORT_RETICULUM_PREVIEW_AGENT_ENDPOINT, - value.as_str(), - ) - }) - .transpose()?; + .transpose()?; return Ok(TransportConfig { profile, source: TransportConfigSource::Environment, @@ -1032,10 +1011,10 @@ fn resolve_transport_config( }) .transpose()? .unwrap_or_default(), - reticulum_preview_behavior, - reticulum_preview_scope, - reticulum_preview_agent_endpoint, - proxy: resolve_transport_proxy_env(env, env_file)?, + reticulum_behavior, + reticulum_scope, + reticulum_agent_endpoint, + radrootsd_execution: resolve_radrootsd_execution_env(env, env_file)?, }); } @@ -1059,10 +1038,10 @@ fn resolve_transport_config( profile: TransportProfileKind::LocalOnly, source: TransportConfigSource::Defaults, nostr_relay_urls: Vec::new(), - reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts, - reticulum_preview_scope: RADROOTS_RETICULUM_PREVIEW_SCOPE_ID.to_owned(), - reticulum_preview_agent_endpoint: None, - proxy: ProxyTransportConfig::default(), + reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts, + reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(), + reticulum_agent_endpoint: None, + radrootsd_execution: RadrootsdExecutionConfig::default(), }) } @@ -1073,8 +1052,8 @@ fn resolve_transport_file_config( ) -> Result<TransportConfig, RuntimeError> { let Some(profile_value) = transport.profile.as_deref() else { if transport.nostr.is_some() - || transport.reticulum_preview.is_some() - || transport.proxy.is_some() + || transport.reticulum.is_some() + || transport.radrootsd_execution.is_some() { return Err(RuntimeError::Config(format!( "{source_label}.profile must be set when profile-specific transport tables are present" @@ -1084,10 +1063,10 @@ fn resolve_transport_file_config( profile: TransportProfileKind::LocalOnly, source, nostr_relay_urls: Vec::new(), - reticulum_preview_behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts, - reticulum_preview_scope: RADROOTS_RETICULUM_PREVIEW_SCOPE_ID.to_owned(), - reticulum_preview_agent_endpoint: None, - proxy: ProxyTransportConfig::default(), + reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts, + reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(), + reticulum_agent_endpoint: None, + radrootsd_execution: RadrootsdExecutionConfig::default(), }); }; let profile = @@ -1099,37 +1078,31 @@ fn resolve_transport_file_config( .map(|urls| normalize_relay_urls(urls, format!("{source_label}.nostr.relay_urls").as_str())) .transpose()? .unwrap_or_default(); - let reticulum_preview_behavior = transport - .reticulum_preview + let reticulum_behavior = transport + .reticulum .as_ref() - .and_then(|preview| preview.behavior.as_deref()) + .and_then(|reticulum| reticulum.behavior.as_deref()) .map(|value| { - parse_reticulum_preview_behavior( - format!("{source_label}.reticulum_preview.behavior").as_str(), - value, - ) + parse_reticulum_behavior(format!("{source_label}.reticulum.behavior").as_str(), value) }) .transpose()? - .unwrap_or(ReticulumPreviewBehavior::RejectDeliveryAttempts); - let reticulum_preview_scope = transport - .reticulum_preview + .unwrap_or(ReticulumBehavior::RejectDeliveryAttempts); + let reticulum_scope = transport + .reticulum .as_ref() - .and_then(|preview| preview.scope.as_deref()) + .and_then(|reticulum| reticulum.scope.as_deref()) .map(|value| { - parse_reticulum_preview_scope( - format!("{source_label}.reticulum_preview.scope").as_str(), - value, - ) + parse_reticulum_scope(format!("{source_label}.reticulum.scope").as_str(), value) }) .transpose()? - .unwrap_or_else(|| RADROOTS_RETICULUM_PREVIEW_SCOPE_ID.to_owned()); - let reticulum_preview_agent_endpoint = transport - .reticulum_preview + .unwrap_or_else(|| RADROOTS_RETICULUM_SCOPE_ID.to_owned()); + let reticulum_agent_endpoint = transport + .reticulum .as_ref() - .and_then(|preview| preview.agent_endpoint.as_deref()) + .and_then(|reticulum| reticulum.agent_endpoint.as_deref()) .map(|value| { - parse_reticulum_preview_agent_endpoint( - format!("{source_label}.reticulum_preview.agent_endpoint").as_str(), + parse_reticulum_agent_endpoint( + format!("{source_label}.reticulum.agent_endpoint").as_str(), value, ) }) @@ -1138,53 +1111,61 @@ fn resolve_transport_file_config( profile, source, nostr_relay_urls, - reticulum_preview_behavior, - reticulum_preview_scope, - reticulum_preview_agent_endpoint, - proxy: resolve_transport_proxy_file(transport.proxy.as_ref(), source_label)?, + reticulum_behavior, + reticulum_scope, + reticulum_agent_endpoint, + radrootsd_execution: resolve_radrootsd_execution_file( + transport.radrootsd_execution.as_ref(), + source_label, + )?, }) } -fn resolve_transport_proxy_env( +fn resolve_radrootsd_execution_env( env: &dyn Environment, env_file: &EnvFileValues, -) -> Result<ProxyTransportConfig, RuntimeError> { - let url = env_value(env, env_file, &[ENV_CLI_TRANSPORT_PROXY_URL]) +) -> Result<RadrootsdExecutionConfig, RuntimeError> { + let url = env_value(env, env_file, &[ENV_CLI_RADROOTSD_EXECUTION_URL]) .unwrap_or_else(|| DEFAULT_RPC_URL.to_owned()); let token_file = - env_value(env, env_file, &[ENV_CLI_TRANSPORT_PROXY_TOKEN_FILE]).map(PathBuf::from); - let token_secret_id = env_value(env, env_file, &[ENV_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID]); - validate_proxy_token_source( + env_value(env, env_file, &[ENV_CLI_RADROOTSD_EXECUTION_TOKEN_FILE]).map(PathBuf::from); + let token_secret_id = env_value( + env, + env_file, + &[ENV_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID], + ); + validate_radrootsd_execution_token_source( token_file.as_ref(), token_secret_id.as_deref(), - "environment proxy transport", + "environment radrootsd execution", )?; - Ok(ProxyTransportConfig { + Ok(RadrootsdExecutionConfig { url: validate_rpc_url(url.as_str())?, token_file, token_secret_id, }) } -fn resolve_transport_proxy_file( - proxy: Option<&TransportProxyFileConfig>, +fn resolve_radrootsd_execution_file( + radrootsd_execution: Option<&RadrootsdExecutionFileConfig>, source_label: &str, -) -> Result<ProxyTransportConfig, RuntimeError> { - let url = proxy - .and_then(|proxy| proxy.url.clone()) +) -> Result<RadrootsdExecutionConfig, RuntimeError> { + let url = radrootsd_execution + .and_then(|execution| execution.url.clone()) .unwrap_or_else(|| DEFAULT_RPC_URL.to_owned()); - let token_file = proxy.and_then(|proxy| proxy.token_file.clone()); - let token_secret_id = proxy.and_then(|proxy| proxy.token_secret_id.clone()); - validate_proxy_token_source( + let token_file = radrootsd_execution.and_then(|execution| execution.token_file.clone()); + let token_secret_id = + radrootsd_execution.and_then(|execution| execution.token_secret_id.clone()); + validate_radrootsd_execution_token_source( token_file.as_ref(), token_secret_id.as_deref(), - format!("{source_label}.proxy").as_str(), + format!("{source_label}.radrootsd_execution").as_str(), )?; - Ok(ProxyTransportConfig { + Ok(RadrootsdExecutionConfig { url: validate_rpc_url(url.as_str()).map_err(|error| match error { - RuntimeError::Config(message) => { - RuntimeError::Config(format!("{source_label}.proxy.url is invalid: {message}")) - } + RuntimeError::Config(message) => RuntimeError::Config(format!( + "{source_label}.radrootsd_execution.url is invalid: {message}" + )), other => other, })?, token_file, @@ -1192,7 +1173,7 @@ fn resolve_transport_proxy_file( }) } -fn validate_proxy_token_source( +fn validate_radrootsd_execution_token_source( token_file: Option<&PathBuf>, token_secret_id: Option<&str>, source_label: &str, @@ -1607,9 +1588,9 @@ fn resolve_mesh_config( fn parse_mesh_scope(source: &str, value: &str) -> Result<MeshScope, RuntimeError> { match value.trim().to_ascii_lowercase().as_str() { "disabled" => Ok(MeshScope::Disabled), - "local_preview" => Ok(MeshScope::LocalPreview), + "local" => Ok(MeshScope::Local), other => Err(RuntimeError::Config(format!( - "{source} must be `disabled` or `local_preview`, got `{other}`" + "{source} must be `disabled` or `local`, got `{other}`" ))), } } @@ -1955,38 +1936,31 @@ fn parse_transport_profile_kind( match value.trim().to_ascii_lowercase().as_str() { "local_only" => Ok(TransportProfileKind::LocalOnly), "nostr" => Ok(TransportProfileKind::Nostr), - "reticulum_preview" => Ok(TransportProfileKind::ReticulumPreview), - "hybrid" => Ok(TransportProfileKind::Hybrid), - "proxy" => Ok(TransportProfileKind::Proxy), + "reticulum" => Ok(TransportProfileKind::Reticulum), + "multi_target" => Ok(TransportProfileKind::MultiTarget), other => Err(RuntimeError::Config(format!( - "{source} must be `local_only`, `nostr`, `reticulum_preview`, `hybrid`, or `proxy`, got `{other}`" + "{source} must be `local_only`, `nostr`, `reticulum`, or `multi_target`, got `{other}`" ))), } } -fn parse_reticulum_preview_behavior( - source: &str, - value: &str, -) -> Result<ReticulumPreviewBehavior, RuntimeError> { +fn parse_reticulum_behavior(source: &str, value: &str) -> Result<ReticulumBehavior, RuntimeError> { match value.trim().to_ascii_lowercase().as_str() { - "reject_delivery_attempts" => Ok(ReticulumPreviewBehavior::RejectDeliveryAttempts), - "defer_delivery_plans" => Ok(ReticulumPreviewBehavior::DeferDeliveryPlans), + "reject_delivery_attempts" => Ok(ReticulumBehavior::RejectDeliveryAttempts), + "defer_delivery_plans" => Ok(ReticulumBehavior::DeferDeliveryPlans), other => Err(RuntimeError::Config(format!( "{source} must be `reject_delivery_attempts` or `defer_delivery_plans`, got `{other}`" ))), } } -fn parse_reticulum_preview_scope(source: &str, value: &str) -> Result<String, RuntimeError> { +fn parse_reticulum_scope(source: &str, value: &str) -> Result<String, RuntimeError> { RadrootsTransportMeshScopeId::parse(value) .map(|scope| scope.as_str().to_owned()) .map_err(|error| RuntimeError::Config(format!("{source} is invalid: {error}"))) } -fn parse_reticulum_preview_agent_endpoint( - source: &str, - value: &str, -) -> Result<String, RuntimeError> { +fn parse_reticulum_agent_endpoint(source: &str, value: &str) -> Result<String, RuntimeError> { if value.is_empty() || value != value.trim() || value @@ -2431,7 +2405,7 @@ mod tests { "old-identity.json".to_owned(), ), ("RADROOTS_SIGNER".to_owned(), "myc".to_owned()), - ("RADROOTS_PUBLISH_MODE".to_owned(), "proxy".to_owned()), + ("RADROOTS_PUBLISH_MODE".to_owned(), "legacy".to_owned()), ( "RADROOTS_RELAYS".to_owned(), "wss://old-relay.example".to_owned(), @@ -2740,7 +2714,7 @@ path = "identity/from-toml.json" .expect_err("invalid transport profile"); assert!(error.to_string().contains("RADROOTS_CLI_TRANSPORT_PROFILE")); assert!(error.to_string().contains("local_only")); - assert!(error.to_string().contains("reticulum_preview")); + assert!(error.to_string().contains("reticulum")); let args = RuntimeInvocationArgs { myc_status_timeout_ms: Some(0), @@ -2953,7 +2927,7 @@ RADROOTS_CLI_LOGGING_STDOUT=true } #[test] - fn hybrid_transport_config_uses_nostr_targets_and_reticulum_preview_behavior() { + fn multi_target_transport_config_uses_nostr_targets_and_reticulum_behavior() { let temp = tempdir().expect("tempdir"); let workspace_root = temp.path().join("workspace"); let repo_local_root = workspace_root.join("infra/local/runtime/radroots"); @@ -2963,16 +2937,19 @@ RADROOTS_CLI_LOGGING_STDOUT=true fs::create_dir_all(&app_config_dir).expect("app config dir"); fs::write( app_config_dir.join("config.toml"), - "[transport]\nprofile = \"hybrid\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.user\", \"wss://relay.backup\"]\n\n[transport.reticulum_preview]\nbehavior = \"defer_delivery_plans\"\nscope = \"farmers_market\"\nagent_endpoint = \"reticulum-agent:local\"\n", + "[transport]\nprofile = \"multi_target\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.user\", \"wss://relay.backup\"]\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\nscope = \"farmers_market\"\nagent_endpoint = \"reticulum-agent:local\"\n", ) .expect("write user config"); let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new()); let resolved = RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default()) - .expect("resolve hybrid transport profile"); + .expect("resolve multi-target transport profile"); - assert_eq!(resolved.transport.profile, TransportProfileKind::Hybrid); + assert_eq!( + resolved.transport.profile, + TransportProfileKind::MultiTarget + ); assert_eq!(resolved.transport.source, TransportConfigSource::UserConfig); assert_eq!( resolved.transport.nostr_relay_urls, @@ -2982,15 +2959,12 @@ RADROOTS_CLI_LOGGING_STDOUT=true ] ); assert_eq!( - resolved.transport.reticulum_preview_behavior, - super::ReticulumPreviewBehavior::DeferDeliveryPlans + resolved.transport.reticulum_behavior, + super::ReticulumBehavior::DeferDeliveryPlans ); - assert_eq!(resolved.transport.reticulum_preview_scope, "farmers_market"); + assert_eq!(resolved.transport.reticulum_scope, "farmers_market"); assert_eq!( - resolved - .transport - .reticulum_preview_agent_endpoint - .as_deref(), + resolved.transport.reticulum_agent_endpoint.as_deref(), Some("reticulum-agent:local") ); } @@ -3006,7 +2980,7 @@ RADROOTS_CLI_LOGGING_STDOUT=true fs::create_dir_all(&app_config_dir).expect("app config dir"); fs::write( repo_local_root.join("config.toml"), - "[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\nurl = \"http://127.0.0.1:7070\"\n", + "[transport]\nprofile = \"reticulum\"\n", ) .expect("write workspace config"); fs::write( @@ -3021,13 +2995,13 @@ RADROOTS_CLI_LOGGING_STDOUT=true user_home.clone(), BTreeMap::from([( "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(), - "proxy".to_owned(), + "reticulum".to_owned(), )]), ); let resolved = RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default()) .expect("resolve environment transport profile"); - assert_eq!(resolved.transport.profile, TransportProfileKind::Proxy); + assert_eq!(resolved.transport.profile, TransportProfileKind::Reticulum); assert_eq!( resolved.transport.source, TransportConfigSource::Environment @@ -3053,7 +3027,7 @@ RADROOTS_CLI_LOGGING_STDOUT=true let resolved = RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default()) .expect("resolve workspace transport profile"); - assert_eq!(resolved.transport.profile, TransportProfileKind::Proxy); + assert_eq!(resolved.transport.profile, TransportProfileKind::Reticulum); assert_eq!( resolved.transport.source, TransportConfigSource::WorkspaceConfig @@ -3269,7 +3243,7 @@ RADROOTS_CLI_LOGGING_STDOUT=true let message = error.to_string(); assert!(message.contains("workspace config [transport].profile")); assert!(message.contains("local_only")); - assert!(message.contains("reticulum_preview")); + assert!(message.contains("reticulum")); } #[test] @@ -3359,7 +3333,7 @@ target = "bin/user-hyfd" r#" [[capability_binding]] capability = "write_plane.trade_jsonrpc" - provider = "legacy_proxy" + provider = "legacy_provider" target_kind = "explicit_endpoint" target = "https://rpc.workspace.test/jsonrpc" "#, diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -623,7 +623,7 @@ fn transport_farm_publish_readiness( ) -> FarmPublishReadiness { if matches!( config.transport.profile, - TransportProfileKind::Nostr | TransportProfileKind::Hybrid + TransportProfileKind::Nostr | TransportProfileKind::MultiTarget ) && config.transport.nostr_relay_urls.is_empty() { return FarmPublishReadiness { @@ -977,11 +977,45 @@ fn component_idempotency_key( args.idempotency_key .as_deref() .map(|value| { - required_text(value, "idempotency_key").map(|key| format!("{key}:{component}")) + required_text(value, "idempotency_key") + .and_then(|key| derive_component_idempotency_key(key.as_str(), component)) }) .transpose() } +fn derive_component_idempotency_key(key: &str, component: &str) -> Result<String, RuntimeError> { + if !is_uuid_v7_idempotency_key(key) { + return Err(RuntimeError::Config( + "idempotency_key must be a UUIDv7".to_owned(), + )); + } + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + for byte in key + .bytes() + .chain(std::iter::once(b':')) + .chain(component.bytes()) + { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + Ok(format!("{}{:012x}", &key[..24], hash & 0xffff_ffff_ffff)) +} + +fn is_uuid_v7_idempotency_key(value: &str) -> bool { + let bytes = value.as_bytes(); + bytes.len() == 36 + && bytes[8] == b'-' + && bytes[13] == b'-' + && bytes[18] == b'-' + && bytes[23] == b'-' + && bytes[14] == b'7' + && matches!(bytes[19], b'8' | b'9' | b'a' | b'b') + && bytes + .iter() + .enumerate() + .all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit()) +} + fn preview_component( rpc_method: &str, event_kind: u32, diff --git a/src/runtime/find.rs b/src/runtime/find.rs @@ -1,5 +1,5 @@ use radroots_replica_store::ReplicaSql; -use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::SqlxSqliteExecutor; use crate::cli::global::FindQueryArgs; use crate::runtime::RuntimeError; @@ -62,7 +62,7 @@ pub fn search(config: &RuntimeConfig, args: &FindQueryArgs) -> Result<FindView, } refresh_market_if_needed(config)?; - let db = ReplicaSql::new(SqliteExecutor::open(&config.local.replica_store_path)?); + let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?); let freshness = freshness_for_scope_from_executor(config, db.executor(), RelayIngestScope::MarketRefresh)?; let applied_query_rewrite = attempt_query_rewrite(config, query.as_str(), &args.query); @@ -162,7 +162,7 @@ fn refresh_market_if_needed(config: &RuntimeConfig) -> Result<(), RuntimeError> if config.output.dry_run || config.transport.nostr_relay_urls.is_empty() { return Ok(()); } - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; let freshness = freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketRefresh)?; if freshness_requires_refresh(&freshness) { diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -13,7 +13,7 @@ use radroots_core::{ use radroots_event::contract::RadrootsActorRole; use radroots_event::farm::RadrootsFarmRef; use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId}; -use radroots_event::kinds::{KIND_LISTING, KIND_LISTING_DRAFT}; +use radroots_event::kinds::KIND_LISTING; use radroots_event::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingPublicLocation, @@ -30,8 +30,8 @@ use radroots_sdk::{ ListingPublishPlan, PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind, SdkMutationState, }; -use radroots_sql_core::SqliteExecutor; -use radroots_trade::listing::{RadrootsListingDraftDocumentV1, validation::validate_listing_event}; +use radroots_sql_core::SqlxSqliteExecutor; +use radroots_trade::listing::{RadrootsListingEditDocumentV1, validation::validate_listing_event}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -237,7 +237,7 @@ struct CanonicalListingDraft { struct SdkListingPublishInput { canonical: CanonicalListingDraft, actor: RadrootsActorContext, - document: RadrootsListingDraftDocumentV1, + document: RadrootsListingEditDocumentV1, } #[derive(Debug, Clone)] @@ -565,7 +565,7 @@ pub fn validate( match canonicalize_draft(&parsed, &contents, &context) { Ok(canonical) => { - let parts = match to_wire_parts_with_kind(&canonical.listing, KIND_LISTING_DRAFT) { + let parts = match to_wire_parts_with_kind(&canonical.listing, KIND_LISTING) { Ok(parts) => parts, Err(error) => { return Ok(invalid_validation_view( @@ -1153,7 +1153,7 @@ fn listing_ready_issues( canonical: &CanonicalListingDraft, contents: &str, ) -> Vec<ListingValidationIssueView> { - let parts = match to_wire_parts_with_kind(&canonical.listing, KIND_LISTING_DRAFT) { + let parts = match to_wire_parts_with_kind(&canonical.listing, KIND_LISTING) { Ok(parts) => parts, Err(error) => { return vec![ListingValidationIssueView { @@ -1615,7 +1615,7 @@ pub fn get( ) -> Result<ListingGetView, RuntimeError> { refresh_market_listing_if_needed(config)?; let freshness = if config.local.replica_store_path.exists() { - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketRefresh)? } else { missing_freshness() @@ -1648,7 +1648,7 @@ pub fn get( }); } - let db = ReplicaSql::new(SqliteExecutor::open(&config.local.replica_store_path)?); + let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?); let rows = db.trade_product_lookup(args.key.as_str())?; let Some(row) = rows.into_iter().next() else { return Ok(ListingGetView { @@ -1735,7 +1735,7 @@ fn refresh_market_listing_if_needed(config: &RuntimeConfig) -> Result<(), Runtim { return Ok(()); } - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; let freshness = freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketRefresh)?; if crate::runtime::sync::freshness_requires_refresh(&freshness) { @@ -1846,7 +1846,7 @@ fn sdk_listing_publish_input( [RadrootsActorRole::Seller], ) .map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?; - let document = RadrootsListingDraftDocumentV1::new(canonical.listing.clone()); + let document = RadrootsListingEditDocumentV1::new(canonical.listing.clone()); Ok(SdkListingPublishInput { canonical, actor, @@ -1973,7 +1973,7 @@ fn listing_validation_event( id: "0".repeat(64), author: canonical.seller_pubkey.clone(), created_at: 0, - kind: KIND_LISTING_DRAFT, + kind: KIND_LISTING, tags: parts.tags, content: parts.content, sig: "0".repeat(128), @@ -2185,7 +2185,7 @@ fn mutate_via_sdk_from_canonical( [RadrootsActorRole::Seller], ) .map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?; - let document = RadrootsListingDraftDocumentV1::new(canonical.listing.clone()); + let document = RadrootsListingEditDocumentV1::new(canonical.listing.clone()); if config.output.dry_run { let session = CliSdkSession::connect_memory(config)?; let plan = session @@ -3428,7 +3428,6 @@ mod tests { | PushOutboxTargetOutcomeKind::TargetUriRejected | PushOutboxTargetOutcomeKind::SkippedAlreadyAccepted | PushOutboxTargetOutcomeKind::DeferredUntilImplemented - | PushOutboxTargetOutcomeKind::PreviewUnavailable | PushOutboxTargetOutcomeKind::Unknown => PushOutboxTransportOutcomeKind::Rejected, _ => PushOutboxTransportOutcomeKind::Rejected, }) diff --git a/src/runtime/mesh.rs b/src/runtime/mesh.rs @@ -1,5 +1,5 @@ use radroots_mesh::{ - RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, RadrootsMeshAdmissionInput, RadrootsMeshPayloadPolicy, + RADROOTS_MESH_UNAVAILABLE_MESSAGE, RadrootsMeshAdmissionInput, RadrootsMeshPayloadPolicy, RadrootsMeshPrivacyClass, RadrootsMeshScope, }; use serde_json::Value as JsonValue; @@ -26,7 +26,7 @@ pub fn set_scope( .and_then(JsonValue::as_str) .unwrap_or("disabled"); match scope { - "disabled" | "local_preview" => {} + "disabled" | "local" => {} other => { return Err(RuntimeError::Config(format!( "mesh scope `{other}` is not supported" @@ -47,14 +47,14 @@ pub fn status(config: &RuntimeConfig) -> MeshStatusView { scope: scope.to_owned(), transport: "reticulum".to_owned(), configured: scope != "disabled", - implementation: "preview_unavailable".to_owned(), + implementation: "unavailable".to_owned(), usable_for_delivery: false, - message: RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE.to_owned(), + message: RADROOTS_MESH_UNAVAILABLE_MESSAGE.to_owned(), } } pub fn policy_check(config: &RuntimeConfig) -> MeshPolicyCheckView { - let policy = RadrootsMeshPayloadPolicy::preview_unavailable(); + let policy = RadrootsMeshPayloadPolicy::reticulum_unavailable(); let privacy_class = RadrootsMeshPrivacyClass::PublicEvent; let input = RadrootsMeshAdmissionInput::new(RadrootsMeshScope::Local, privacy_class, 1, 1); let decision = policy.evaluate(&input); @@ -88,8 +88,8 @@ fn scope_view(scope: &str, state: &str) -> MeshScopeView { state: state.to_owned(), source: MESH_SOURCE.to_owned(), scope: scope.to_owned(), - implementation: "preview_unavailable".to_owned(), - message: "Mesh delivery is disabled unless a preview scope is explicitly configured" + implementation: "unavailable".to_owned(), + message: "Mesh delivery is disabled unless a Reticulum scope is explicitly configured" .to_owned(), actions: vec!["radroots mesh policy check".to_owned()], } diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -16,17 +16,16 @@ use radroots_event::RadrootsEventPtr; use radroots_event::contract::RadrootsActorRole; use radroots_event::ids::{ RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, - RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey, + RadrootsOrderQuoteId, RadrootsPublicKey, }; use radroots_event::kinds::{ KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, - KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, }; use radroots_event::order::{ RadrootsOrderEconomicActor, RadrootsOrderEconomicEffect, RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomicLineKind, RadrootsOrderEconomics, RadrootsOrderEventType, RadrootsOrderInventoryCommitment, RadrootsOrderItem, - RadrootsOrderPricingBasis, RadrootsOrderRequest, RadrootsOrderRevisionOutcome, + RadrootsOrderPricingBasis, RadrootsOrderRequest, }; use radroots_event_codec::d_tag::is_d_tag_base64url; use radroots_event_codec::order::{order_event_context_from_tags, order_request_from_event}; @@ -54,12 +53,10 @@ use radroots_sdk::{ SdkMutationState, SdkTradeStatusSource, TargetPolicy, TradeAcceptRequest, TradeCancelRequest, TradeCancellationPlan, TradeCancellationReceipt, TradeDecisionPlan, TradeDecisionReceipt, TradeDeclineRequest, TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest, - TradeRevisionDecisionPlan, TradeRevisionDecisionReceipt, TradeRevisionDecisionRequest, - TradeRevisionProposalPlan, TradeRevisionProposalReceipt, TradeRevisionProposalRequest, TradeStatusReceipt, TradeStatusRequest, TradeSubmitPlan, TradeSubmitReceipt, TradeWorkflowEnqueueReceipt, }; -use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::SqlxSqliteExecutor; use radroots_trade::identity::RadrootsTradeLocator; use radroots_trade::order::canonicalize_order_request_for_signer; use radroots_transport_nostr::{ @@ -226,6 +223,7 @@ fn protocol_listing_addr(value: &str, field: &str) -> Result<RadrootsListingAddr }) } +#[cfg(any())] fn protocol_revision_id(value: &str, field: &str) -> Result<RadrootsOrderRevisionId, RuntimeError> { value.parse().map_err(|error| { RuntimeError::Config(format!("{field} is not a valid trade revision id: {error}")) @@ -1241,18 +1239,12 @@ pub fn revision_propose( if let Some(view) = order_revision_args_preflight_view(config, args) { return Ok(view); } - let seller = match account::resolve_account(config)? { - Some(account) => account, - None => { - let mut view = - order_revision_base_view(config, args, "unconfigured", config.output.dry_run); - view.reason = - Some("trade revision propose requires a selected seller account".to_owned()); - view.actions = vec!["radroots account create".to_owned()]; - return Ok(view); - } - }; - propose_revision_via_sdk(config, args, &seller) + let mut view = order_revision_base_view(config, args, "unavailable", config.output.dry_run); + view.reason = Some( + "trade revision proposal is not supported by the current SDK trade runtime".to_owned(), + ); + view.actions = vec![format!("radroots trade status get {}", args.key)]; + Ok(view) } pub fn revision_decide( @@ -1262,22 +1254,13 @@ pub fn revision_decide( if let Some(view) = order_revision_decision_args_preflight_view(config, args) { return Ok(view); } - let actor_context = match order_buyer_write_actor_context(config, args.key.as_str())? { - Some(context) => context, - None => { - let mut view = order_revision_decision_base_view( - config, - args, - "unconfigured", - config.output.dry_run, - ); - view.reason = - Some("trade revision decision requires a selected buyer account".to_owned()); - view.actions = vec!["radroots account create".to_owned()]; - return Ok(view); - } - }; - decide_revision_via_sdk(config, args, actor_context) + let mut view = + order_revision_decision_base_view(config, args, "unavailable", config.output.dry_run); + view.reason = Some( + "trade revision decision is not supported by the current SDK trade runtime".to_owned(), + ); + view.actions = vec![format!("radroots trade status get {}", args.key)]; + Ok(view) } pub fn cancel( @@ -1372,6 +1355,7 @@ fn decide_trade_via_sdk( )) } +#[cfg(any())] fn propose_revision_via_sdk( config: &RuntimeConfig, args: &TradeRevisionProposeArgs, @@ -1411,6 +1395,7 @@ fn propose_revision_via_sdk( )) } +#[cfg(any())] fn decide_revision_via_sdk( config: &RuntimeConfig, args: &TradeRevisionDecisionArgs, @@ -1547,6 +1532,7 @@ fn inventory_commitments_from_status( .collect()) } +#[cfg(any())] #[derive(Debug, Clone)] struct SdkRevisionRequestParts { revision_id: RadrootsOrderRevisionId, @@ -1554,6 +1540,7 @@ struct SdkRevisionRequestParts { economics: RadrootsOrderEconomics, } +#[cfg(any())] fn revision_request_parts_from_status( args: &TradeRevisionProposeArgs, status: &OrderStatusView, @@ -1610,6 +1597,7 @@ fn sdk_trade_decision_outcome_view( } } +#[cfg(any())] fn sdk_trade_revision_outcome_view( config: &RuntimeConfig, args: &TradeRevisionProposeArgs, @@ -1655,6 +1643,7 @@ fn sdk_trade_revision_outcome_view( } } +#[cfg(any())] fn sdk_trade_revision_decision_outcome_view( config: &RuntimeConfig, args: &TradeRevisionDecisionArgs, @@ -2109,6 +2098,7 @@ fn apply_order_decision_status(view: &mut OrderDecisionView, status: &OrderStatu view.inventory = status.inventory.clone(); } +#[cfg(any())] fn apply_order_revision_status(view: &mut OrderRevisionProposalView, status: &OrderStatusView) { view.order_id = status.order_id.clone(); view.locator = order_locator_view_from_status(status); @@ -2130,6 +2120,7 @@ fn apply_order_revision_status(view: &mut OrderRevisionProposalView, status: &Or view.issues = status.reducer_issues.clone(); } +#[cfg(any())] fn apply_order_revision_decision_status( view: &mut OrderRevisionDecisionView, status: &OrderStatusView, @@ -2259,6 +2250,7 @@ fn order_revision_decision_args_preflight_view( Some(view) } +#[cfg(any())] fn revised_order_economics( args: &TradeRevisionProposeArgs, revision_id: &str, @@ -2325,6 +2317,7 @@ fn revised_order_economics( Ok(economics) } +#[cfg(any())] fn revision_adjustment_line( args: &TradeRevisionProposeArgs, expected_currency: RadrootsCoreCurrency, @@ -2489,6 +2482,7 @@ fn sdk_order_decision_actions(push_event: Option<&PushOutboxEventReceipt>) -> Ve Vec::new() } +#[cfg(any())] fn sdk_enqueued_order_revision_view( config: &RuntimeConfig, args: &TradeRevisionProposeArgs, @@ -2539,6 +2533,7 @@ fn sdk_enqueued_order_revision_view( view } +#[cfg(any())] fn sdk_enqueued_order_revision_decision_view( config: &RuntimeConfig, args: &TradeRevisionDecisionArgs, @@ -2796,7 +2791,7 @@ fn resolve_order_listing( ))); } - let db = ReplicaSql::new(SqliteExecutor::open(&config.local.replica_store_path)?); + let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?); let rows = db.trade_product_lookup(listing_lookup)?; match rows.len() { 0 => Err(RuntimeError::Config(format!( @@ -2864,7 +2859,7 @@ fn resolve_trade_product_by_listing_addr( return Ok(None); } - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; let product_rows = trade_product::find_many( &executor, &ITradeProductFindMany { @@ -2895,7 +2890,7 @@ fn resolve_active_listing_event_id( return Ok(None); } - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; let product_rows = trade_product::find_many( &executor, &ITradeProductFindMany { @@ -4874,33 +4869,14 @@ fn propose_trade_via_sdk( let actor = sdk_trade_actor(account, RadrootsActorRole::Buyer, "propose")?; let publish_mode = trade_publish_mode(config); let ack_policy = trade_satisfaction_policy(publish_mode)?; - let economics = - loaded.document.order.economics.clone().ok_or_else(|| { - RuntimeError::Config("trade draft is missing quote economics".to_owned()) - })?; - let items = loaded - .document - .order - .items - .iter() - .map(|item| { - Ok(RadrootsOrderItem { - bin_id: protocol_inventory_bin_id(item.bin_id.as_str(), "order item bin_id")?, - bin_count: item.bin_count, - }) - }) - .collect::<Result<Vec<_>, RuntimeError>>()?; + let order_request = canonical_order_request_payload_from_loaded( + loaded, + account.record.public_identity.public_key_hex.as_str(), + )?; let mut request = TradeProposeRequest::new( actor, order_submit_listing_event_ptr(loaded)?, - protocol_order_id(loaded.document.order.order_id.as_str(), "order_id")?, - protocol_listing_addr(loaded.document.order.listing_addr.as_str(), "listing_addr")?, - protocol_pubkey( - loaded.document.order.seller_pubkey.as_str(), - "seller_pubkey", - )?, - items, - economics, + order_request, trade_target_policy(), publish_mode, ack_policy, @@ -5483,6 +5459,7 @@ fn next_order_id() -> String { ) } +#[cfg(any())] fn next_revision_id() -> String { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/src/runtime/order/sdk_status.rs b/src/runtime/order/sdk_status.rs @@ -112,7 +112,7 @@ fn sdk_status_evidence_view( has_decision: evidence.has_decision, has_agreement: evidence.has_agreement, has_validation_receipt: evidence.has_validation_receipt, - has_pending_revision: evidence.has_pending_revision, + has_pending_revision: false, has_cancellation: evidence.has_cancellation, has_issues: evidence.has_issues, } @@ -148,8 +148,8 @@ fn sdk_status_validation_trust_view( fn sdk_status_eligibility_view(eligibility: &TradeStatusEligibility) -> OrderStatusEligibilityView { OrderStatusEligibilityView { can_decide: eligibility.can_decide, - can_propose_revision: eligibility.can_propose_revision, - can_decide_revision: eligibility.can_decide_revision, + can_propose_revision: false, + can_decide_revision: false, can_cancel: eligibility.can_cancel, } } @@ -159,8 +159,7 @@ fn sdk_status_next_action(kind: TradeStatusNextActionKind) -> &'static str { TradeStatusNextActionKind::NoLocalOrder => "no_local_order", TradeStatusNextActionKind::InspectEvidenceIssues => "inspect_evidence_issues", TradeStatusNextActionKind::AwaitSellerDecision => "await_seller_decision", - TradeStatusNextActionKind::DecideRevision => "decide_revision", - TradeStatusNextActionKind::AwaitRhiValidation => "await_rhi_validation", + TradeStatusNextActionKind::AwaitValidation => "await_validation", TradeStatusNextActionKind::Terminal => "terminal", _ => "unknown", } @@ -171,11 +170,11 @@ fn sdk_order_status_state(status: TradeStatusKind) -> &'static str { TradeStatusKind::Missing => "missing", TradeStatusKind::Ambiguous => "ambiguous", TradeStatusKind::Requested => "requested", - TradeStatusKind::RevisionProposed => "revision_proposed", - TradeStatusKind::AgreedPendingRhi => "pending_rhi", + TradeStatusKind::AgreedPendingValidation => "pending_validation", TradeStatusKind::Committed => "committed", TradeStatusKind::Declined => "declined", TradeStatusKind::Cancelled => "cancelled", + TradeStatusKind::ValidationExpired => "validation_expired", TradeStatusKind::Invalid => "invalid", _ => "unknown", } @@ -226,11 +225,11 @@ fn sdk_order_status_lifecycle_phase(receipt: &TradeStatusReceipt) -> &'static st TradeStatusKind::Missing => "missing", TradeStatusKind::Ambiguous => "ambiguous", TradeStatusKind::Requested => "requested", - TradeStatusKind::RevisionProposed => "revision_proposed", - TradeStatusKind::AgreedPendingRhi => "pending_rhi", + TradeStatusKind::AgreedPendingValidation => "pending_validation", TradeStatusKind::Committed => "committed", TradeStatusKind::Declined => "declined", TradeStatusKind::Cancelled => "cancelled", + TradeStatusKind::ValidationExpired => "validation_expired", TradeStatusKind::Invalid => "invalid", _ => "unknown", } @@ -294,7 +293,6 @@ mod tests { has_decision: false, has_agreement: false, has_validation_receipt: false, - has_pending_revision: false, has_cancellation: false, has_issues: false, }, @@ -302,8 +300,6 @@ mod tests { online_evidence: None, eligibility: TradeStatusEligibility { can_decide: false, - can_propose_revision: false, - can_decide_revision: false, can_cancel: false, }, next_action: TradeStatusNextActionKind::InspectEvidenceIssues, @@ -312,7 +308,6 @@ mod tests { decision_event_id: None, agreement_event_id: None, rhi_receipt_event_id: None, - pending_revision_event_id: None, cancellation_event_id: None, last_event_id: None, issues: Vec::new(), diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs @@ -97,24 +97,19 @@ pub fn resolve_write_plane_provider( publish: &PublishRuntimeView, ) -> WritePlaneProviderView { let (provider_runtime_id, binding_model, detail) = match config.transport.profile { - TransportProfileKind::Nostr | TransportProfileKind::Hybrid => ( + TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => ( config.transport.profile.as_str(), "nostr_transport", "configured transport profile publishes through Nostr; readiness is reported under publish", ), - TransportProfileKind::Proxy => ( - "proxy", - "proxy_transport", - "proxy transport profile is selected; readiness is reported under publish", - ), TransportProfileKind::LocalOnly => ( "local_only", "local_transport", "local_only transport profile does not provide network publish", ), - TransportProfileKind::ReticulumPreview => ( - "reticulum_preview", - "reticulum_preview", + TransportProfileKind::Reticulum => ( + "reticulum", + "reticulum_transport", RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, ), }; diff --git a/src/runtime/runtime_store.rs b/src/runtime/runtime_store.rs @@ -11,7 +11,7 @@ use radroots_runtime_store::{ PublishOutboxStatus, RuntimeStore, RuntimeStoreRecord, RuntimeStoreRecordFamily, RuntimeStoreRecordInput, RuntimeStoreRecordStatus, SourceRuntime, }; -use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::SqlxSqliteExecutor; use serde_json::Value; use crate::runtime::RuntimeError; @@ -79,7 +79,7 @@ pub fn list_shared_records_latest( if !database_path.exists() { return Ok(Vec::new()); } - let executor = SqliteExecutor::open(database_path)?; + let executor = SqlxSqliteExecutor::open(database_path)?; let store = RuntimeStore::new(executor); Ok(store.list_records_changed_latest(limit)?) } @@ -94,7 +94,7 @@ pub fn list_shared_records_before( if !database_path.exists() { return Ok(Vec::new()); } - let executor = SqliteExecutor::open(database_path)?; + let executor = SqlxSqliteExecutor::open(database_path)?; let store = RuntimeStore::new(executor); Ok(store.list_records_changed_before(before_change_seq, before_seq, limit)?) } @@ -107,15 +107,16 @@ pub fn get_shared_record( if !database_path.exists() { return Ok(None); } - let executor = SqliteExecutor::open(database_path)?; + let executor = SqlxSqliteExecutor::open(database_path)?; let store = RuntimeStore::new(executor); Ok(store.get_record(record_id)?) } -fn open_store(config: &RuntimeConfig) -> Result<RuntimeStore<SqliteExecutor>, RuntimeError> { +fn open_store(config: &RuntimeConfig) -> Result<RuntimeStore<SqlxSqliteExecutor>, RuntimeError> { let root = shared_runtime_store_root_from_paths(&config.paths)?; fs::create_dir_all(&root)?; - let executor = SqliteExecutor::open(shared_runtime_store_db_path_from_paths(&config.paths)?)?; + let executor = + SqlxSqliteExecutor::open(shared_runtime_store_db_path_from_paths(&config.paths)?)?; let store = RuntimeStore::new(executor); store.migrate_up()?; Ok(store) diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -16,13 +16,13 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectClientTarget, RadrootsNostrConnectError, RadrootsNostrConnectUri, }; use radroots_sdk::{ - HybridProfile, MeshScopeId, NostrProfile, NostrRelayUrlPolicy, ProxyProfile, + MeshScopeId, MultiTargetProfile, NostrProfile, NostrRelayUrlPolicy, PushOutboxTargetOutcomeKind, PushOutboxTransportOutcomeKind, RadrootsClient, RadrootsClientBuilder, RadrootsSdkError, RadrootsSdkLocalKeySigner, RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, RadrootsSdkStorageConfig, - ReticulumPreviewAgentEndpoint, ReticulumPreviewBehavior as SdkReticulumPreviewBehavior, - ReticulumPreviewProfile, TargetPolicy, TradeValidationReceiptNostrRelayOutcomeKind, + RadrootsdExecutionProfile, ReticulumAgentEndpoint, ReticulumBehavior as SdkReticulumBehavior, + ReticulumProfile, TargetPolicy, TradeValidationReceiptNostrRelayOutcomeKind, TradeValidationReceiptNostrRelayTransportOutcomeKind, TransportProfile, }; use radroots_transport_nostr::{ @@ -37,8 +37,8 @@ use url::Url; use crate::runtime::RuntimeError; use crate::runtime::account; use crate::runtime::config::{ - CapabilityBindingTargetKind, ReticulumPreviewBehavior, RuntimeConfig, - SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend, TransportProfileKind, + CapabilityBindingTargetKind, ReticulumBehavior, RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY, + SignerBackend, TransportProfileKind, }; const SDK_STORAGE_DIR_NAME: &str = "sdk"; @@ -79,6 +79,7 @@ pub struct CliSdkConfig { pub geonames_cache_root: PathBuf, pub nostr_relay_url_policy: NostrRelayUrlPolicy, pub transport_profile: TransportProfile, + pub radrootsd_execution_profile: Option<RadrootsdExecutionProfile>, } impl CliSdkConfig { @@ -88,6 +89,7 @@ impl CliSdkConfig { geonames_cache_root: config.paths.shared_cache_root.clone(), nostr_relay_url_policy: sdk_nostr_relay_url_policy(config), transport_profile: sdk_transport_profile(config)?, + radrootsd_execution_profile: sdk_radrootsd_execution_profile(config)?, }) } @@ -97,16 +99,22 @@ impl CliSdkConfig { geonames_cache_root: config.paths.shared_cache_root.clone(), nostr_relay_url_policy: sdk_nostr_relay_url_policy(config), transport_profile: TransportProfile::local_only(), + radrootsd_execution_profile: None, } } pub fn builder(&self) -> RadrootsClientBuilder { - RadrootsClient::builder() + let builder = RadrootsClient::builder() .storage(RadrootsSdkStorageConfig::Directory( self.storage_root.clone(), )) .geonames_cache_root(self.geonames_cache_root.clone()) - .transport_profile(self.transport_profile.clone()) + .transport_profile(self.transport_profile.clone()); + if let Some(profile) = self.radrootsd_execution_profile.clone() { + builder.radrootsd_execution_profile(profile) + } else { + builder + } } } @@ -398,7 +406,7 @@ async fn signer_provider( request_policy, ) .map_err(|error| RuntimeError::Config(error.to_string()))?; - Ok(RadrootsSdkSignerProvider::MycNip46(signer)) + Ok(RadrootsSdkSignerProvider::MycNip46(Box::new(signer))) } } } @@ -674,93 +682,106 @@ fn sdk_transport_profile(config: &RuntimeConfig) -> Result<TransportProfile, Run .map_err(|error| RuntimeError::Config(error.to_string()))?; Ok(TransportProfile::nostr(profile)) } - TransportProfileKind::ReticulumPreview => Ok(TransportProfile::reticulum_preview( - sdk_reticulum_preview_profile(config)?, - )), - TransportProfileKind::Hybrid => { + TransportProfileKind::Reticulum => { + Ok(TransportProfile::reticulum(sdk_reticulum_profile(config)?)) + } + TransportProfileKind::MultiTarget => { let nostr = NostrProfile::new( config.transport.nostr_relay_urls.iter().map(String::as_str), sdk_nostr_relay_url_policy(config), ) .map_err(|error| RuntimeError::Config(error.to_string()))?; - Ok(TransportProfile::hybrid(HybridProfile::new( + Ok(TransportProfile::multi_target(MultiTargetProfile::new( nostr, - sdk_reticulum_preview_profile(config)?, + sdk_reticulum_profile(config)?, ))) } - TransportProfileKind::Proxy => { - let profile = ProxyProfile::new(config.transport.proxy.url.clone()) - .with_bearer_token(proxy_bearer_token(config)?); - Ok(TransportProfile::proxy(profile)) - } } } -fn sdk_reticulum_preview_profile( - config: &RuntimeConfig, -) -> Result<ReticulumPreviewProfile, RuntimeError> { - let behavior = match config.transport.reticulum_preview_behavior { - ReticulumPreviewBehavior::RejectDeliveryAttempts => { - SdkReticulumPreviewBehavior::RejectDeliveryAttempts - } - ReticulumPreviewBehavior::DeferDeliveryPlans => { - SdkReticulumPreviewBehavior::DeferDeliveryPlans - } +fn sdk_reticulum_profile(config: &RuntimeConfig) -> Result<ReticulumProfile, RuntimeError> { + let behavior = match config.transport.reticulum_behavior { + ReticulumBehavior::RejectDeliveryAttempts => SdkReticulumBehavior::RejectDeliveryAttempts, + ReticulumBehavior::DeferDeliveryPlans => SdkReticulumBehavior::DeferDeliveryPlans, }; - let scope = MeshScopeId::parse(config.transport.reticulum_preview_scope.as_str()) + let scope = MeshScopeId::parse(config.transport.reticulum_scope.as_str()) .map_err(|error| RuntimeError::Config(error.to_string()))?; - let mut profile = ReticulumPreviewProfile::preview_unavailable() + let mut profile = ReticulumProfile::deferred_until_implemented() .with_behavior(behavior) .with_scope(scope); - if let Some(agent_endpoint) = config.transport.reticulum_preview_agent_endpoint.as_ref() { + if let Some(agent_endpoint) = config.transport.reticulum_agent_endpoint.as_ref() { profile = profile.with_agent_endpoint( - ReticulumPreviewAgentEndpoint::parse(agent_endpoint.as_str()) + ReticulumAgentEndpoint::parse(agent_endpoint.as_str()) .map_err(|error| RuntimeError::Config(error.to_string()))?, ); } Ok(profile) } -pub(crate) fn validate_proxy_bearer_token(config: &RuntimeConfig) -> Result<(), RuntimeError> { - proxy_bearer_token(config).map(|_| ()) +fn sdk_radrootsd_execution_profile( + config: &RuntimeConfig, +) -> Result<Option<RadrootsdExecutionProfile>, RuntimeError> { + if config.transport.radrootsd_execution.token_file.is_none() + && config + .transport + .radrootsd_execution + .token_secret_id + .is_none() + { + return Ok(None); + } + Ok(Some( + RadrootsdExecutionProfile::new(config.transport.radrootsd_execution.url.clone()) + .with_bearer_token(radrootsd_execution_bearer_token(config)?), + )) } -fn proxy_bearer_token(config: &RuntimeConfig) -> Result<String, RuntimeError> { - if let Some(path) = config.transport.proxy.token_file.as_ref() { +fn radrootsd_execution_bearer_token(config: &RuntimeConfig) -> Result<String, RuntimeError> { + if let Some(path) = config.transport.radrootsd_execution.token_file.as_ref() { let token = fs::read_to_string(path).map_err(|error| { RuntimeError::Config(format!( - "failed to read proxy token file {}: {error}", + "failed to read radrootsd execution token file {}: {error}", path.display() )) })?; - return normalize_proxy_bearer_token( + return normalize_radrootsd_execution_bearer_token( token.as_str(), - format!("proxy token file {}", path.display()).as_str(), + format!("radrootsd execution token file {}", path.display()).as_str(), ); } - if let Some(secret_id) = config.transport.proxy.token_secret_id.as_ref() { + if let Some(secret_id) = config + .transport + .radrootsd_execution + .token_secret_id + .as_ref() + { let vault = account::account_secret_vault(config)?; let token = vault.load_secret(secret_id).map_err(|error| { RuntimeError::Config(format!( - "failed to load proxy token secret `{secret_id}`: {error}" + "failed to load radrootsd execution token secret `{secret_id}`: {error}" )) })?; let token = token.ok_or_else(|| { - RuntimeError::Config(format!("proxy token secret `{secret_id}` was not found")) + RuntimeError::Config(format!( + "radrootsd execution token secret `{secret_id}` was not found" + )) })?; - return normalize_proxy_bearer_token( + return normalize_radrootsd_execution_bearer_token( token.as_str(), - format!("proxy token secret `{secret_id}`").as_str(), + format!("radrootsd execution token secret `{secret_id}`").as_str(), ); } Err(RuntimeError::Config( - "proxy transport profile requires a configured token file or token secret id".to_owned(), + "radrootsd execution requires a configured token file or token secret id".to_owned(), )) } -fn normalize_proxy_bearer_token(raw: &str, source: &str) -> Result<String, RuntimeError> { +fn normalize_radrootsd_execution_bearer_token( + raw: &str, + source: &str, +) -> Result<String, RuntimeError> { let token = raw.trim(); if token.is_empty() { return Err(RuntimeError::Config(format!("{source} is empty"))); @@ -782,8 +803,8 @@ mod tests { use radroots_authority::RadrootsEventSigner; use radroots_sdk::{ - ProxyAuth, PushOutboxTargetOutcomeKind, PushOutboxTransportOutcomeKind, SdkStorageKind, - StorageStatusRequest, TradeValidationReceiptNostrRelayOutcomeKind, + PushOutboxTargetOutcomeKind, PushOutboxTransportOutcomeKind, RadrootsdExecutionAuth, + SdkStorageKind, StorageStatusRequest, TradeValidationReceiptNostrRelayOutcomeKind, TradeValidationReceiptNostrRelayTransportOutcomeKind, }; use radroots_secret_vault::RadrootsSecretBackend; @@ -793,7 +814,7 @@ mod tests { use crate::runtime::config::{ AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, - ReticulumPreviewBehavior, RhiConfig, RpcConfig, SignerBackend, SignerConfig, Verbosity, + ReticulumBehavior, RhiConfig, RpcConfig, SignerBackend, SignerConfig, Verbosity, }; struct DirectRrRsDependency { @@ -871,9 +892,9 @@ mod tests { DirectRrRsDependency { section: "dependencies", name: "radroots_mesh", - owner: "cli-mesh-preview-policy", - reason: "canonical Reticulum preview admission policy and structured delivery denial reporting", - lifecycle: "retain while CLI exposes mesh preview status and policy inspection", + owner: "cli-mesh-reticulum-admission-policy", + reason: "canonical Reticulum admission policy and structured delivery denial reporting", + lifecycle: "retain while CLI exposes Reticulum availability status and policy inspection", }, DirectRrRsDependency { section: "dependencies", @@ -1160,7 +1181,7 @@ mod tests { MigratedCliPathGuard { label: "trade submit", path: "src/runtime/order.rs", - start: "fn propose_trade_via_sdk(", + start: "fn canonical_order_request_payload_from_loaded(", end: "fn sdk_trade_submit_outcome_view(", required_tokens: &[ "propose_trade_via_sdk", @@ -1325,38 +1346,34 @@ mod tests { } #[test] - fn maps_hybrid_runtime_config_to_sdk_hybrid_profile() { + fn maps_multi_target_runtime_config_to_sdk_multi_target_profile() { let root = tempdir().expect("tempdir"); let mut config = sample_config( root.path(), vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()], ); - config.transport.profile = TransportProfileKind::Hybrid; - config.transport.reticulum_preview_behavior = ReticulumPreviewBehavior::DeferDeliveryPlans; - config.transport.reticulum_preview_scope = "farmers_market".to_owned(); - config.transport.reticulum_preview_agent_endpoint = - Some("reticulum-agent:local".to_owned()); + config.transport.profile = TransportProfileKind::MultiTarget; + config.transport.reticulum_behavior = ReticulumBehavior::DeferDeliveryPlans; + config.transport.reticulum_scope = "farmers_market".to_owned(); + config.transport.reticulum_agent_endpoint = Some("reticulum-agent:local".to_owned()); let sdk_config = CliSdkConfig::from_runtime_config(&config).expect("sdk config"); - let TransportProfile::Hybrid { profile } = sdk_config.transport_profile else { - panic!("expected Hybrid transport profile"); + let TransportProfile::MultiTarget { profile } = sdk_config.transport_profile else { + panic!("expected multi-target transport profile"); }; assert_eq!( profile.nostr().relay_urls(), vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()] ); assert_eq!( - profile.reticulum_preview().behavior().as_str(), + profile.reticulum().behavior().as_str(), "defer_delivery_plans" ); - assert_eq!( - profile.reticulum_preview().scope().as_str(), - "farmers_market" - ); + assert_eq!(profile.reticulum().scope().as_str(), "farmers_market"); assert_eq!( profile - .reticulum_preview() + .reticulum() .agent_endpoint() .expect("agent endpoint") .as_str(), @@ -1365,72 +1382,77 @@ mod tests { } #[test] - fn maps_proxy_token_file_to_sdk_profile_auth() { + fn maps_radrootsd_execution_token_file_to_sdk_profile_auth() { let root = tempdir().expect("tempdir"); let mut config = sample_config(root.path(), Vec::new()); - let token_file = root.path().join("proxy.token"); - fs::write(&token_file, "proxy-file-token\n").expect("write token file"); - config.transport.profile = TransportProfileKind::Proxy; - config.transport.proxy.url = "http://127.0.0.1:7070".to_owned(); - config.transport.proxy.token_file = Some(token_file); + let token_file = root.path().join("radrootsd_execution.token"); + fs::write(&token_file, "radrootsd-execution-file-token\n").expect("write token file"); + config.transport.radrootsd_execution.url = "http://127.0.0.1:7070".to_owned(); + config.transport.radrootsd_execution.token_file = Some(token_file); let sdk_config = CliSdkConfig::from_runtime_config(&config).expect("sdk config"); - let TransportProfile::Proxy { profile } = sdk_config.transport_profile else { - panic!("expected proxy transport profile"); - }; + assert!(matches!( + sdk_config.transport_profile, + TransportProfile::LocalOnly + )); + let profile = sdk_config + .radrootsd_execution_profile + .expect("radrootsd execution profile"); assert_eq!(profile.endpoint_url(), "http://127.0.0.1:7070"); assert_eq!( profile.auth(), - &ProxyAuth::BearerToken("proxy-file-token".to_owned()) + &RadrootsdExecutionAuth::BearerToken("radrootsd-execution-file-token".to_owned()) ); } #[test] - fn maps_proxy_token_secret_id_to_sdk_profile_auth() { + fn maps_radrootsd_execution_token_secret_id_to_sdk_profile_auth() { let root = tempdir().expect("tempdir"); let mut config = sample_config(root.path(), Vec::new()); - config.transport.profile = TransportProfileKind::Proxy; - config.transport.proxy.url = "http://127.0.0.1:7070".to_owned(); - config.transport.proxy.token_secret_id = Some("proxy_token".to_owned()); + config.transport.radrootsd_execution.url = "http://127.0.0.1:7070".to_owned(); + config.transport.radrootsd_execution.token_secret_id = + Some("radrootsd_execution_token".to_owned()); let vault = account::account_secret_vault(&config).expect("account vault"); vault - .store_secret("proxy_token", "proxy-secret-token") - .expect("store proxy token"); + .store_secret( + "radrootsd_execution_token", + "radrootsd-execution-secret-token", + ) + .expect("store radrootsd execution token"); let sdk_config = CliSdkConfig::from_runtime_config(&config).expect("sdk config"); - let TransportProfile::Proxy { profile } = sdk_config.transport_profile else { - panic!("expected proxy transport profile"); - }; + let profile = sdk_config + .radrootsd_execution_profile + .expect("radrootsd execution profile"); assert_eq!( profile.auth(), - &ProxyAuth::BearerToken("proxy-secret-token".to_owned()) + &RadrootsdExecutionAuth::BearerToken("radrootsd-execution-secret-token".to_owned()) ); } #[test] - fn proxy_sdk_profile_requires_materialized_bearer_token() { + fn radrootsd_execution_profile_requires_materialized_bearer_token() { let root = tempdir().expect("tempdir"); let mut config = sample_config(root.path(), Vec::new()); - config.transport.profile = TransportProfileKind::Proxy; - config.transport.proxy.url = "http://127.0.0.1:7070".to_owned(); + config.transport.radrootsd_execution.url = "http://127.0.0.1:7070".to_owned(); - assert!(matches!( - CliSdkConfig::from_runtime_config(&config), - Err(RuntimeError::Config(message)) - if message.contains("configured token file or token secret id") - )); + let sdk_config = CliSdkConfig::from_runtime_config(&config).expect("sdk config"); + assert!(sdk_config.radrootsd_execution_profile.is_none()); } #[test] - fn proxy_token_resolution_rejects_empty_or_header_unsafe_tokens() { + fn radrootsd_execution_token_resolution_rejects_empty_or_header_unsafe_tokens() { assert!(matches!( - normalize_proxy_bearer_token(" \n", "proxy token"), + normalize_radrootsd_execution_bearer_token(" \n", "radrootsd execution token"), Err(RuntimeError::Config(message)) if message.contains("empty") )); assert!(matches!( - normalize_proxy_bearer_token("proxy\nsecret", "proxy token"), + normalize_radrootsd_execution_bearer_token( + "radrootsd\nexecution", + "radrootsd execution token" + ), Err(RuntimeError::Config(message)) if message.contains("control characters") )); } @@ -1597,10 +1619,6 @@ mod tests { PushOutboxTargetOutcomeKind::DeferredUntilImplemented, "deferred_until_implemented", ), - ( - PushOutboxTargetOutcomeKind::PreviewUnavailable, - "preview_unavailable", - ), (PushOutboxTargetOutcomeKind::Unknown, "unknown"), ] { assert_eq!(sdk_target_outcome_kind_label(kind), label); diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs @@ -11,7 +11,7 @@ use crate::view::runtime::{ }; use radroots_event::kinds::{ KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, - KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_PROFILE, + KIND_PROFILE, }; use radroots_nostr_accounts::prelude::RadrootsNostrAccountStatus; use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions; @@ -294,7 +294,7 @@ fn disabled_binding_status() -> SignerBindingStatusView { } } -fn cli_write_kinds() -> [CliWriteKind; 12] { +fn cli_write_kinds() -> [CliWriteKind; 9] { [ CliWriteKind { command: "sync.push", @@ -332,18 +332,6 @@ fn cli_write_kinds() -> [CliWriteKind; 12] { command: "trade.cancel", event_kind: KIND_ORDER_CANCELLATION, }, - CliWriteKind { - command: "trade.revision.propose", - event_kind: KIND_ORDER_REVISION_PROPOSAL, - }, - CliWriteKind { - command: "trade.revision.accept", - event_kind: KIND_ORDER_REVISION_DECISION, - }, - CliWriteKind { - command: "trade.revision.decline", - event_kind: KIND_ORDER_REVISION_DECISION, - }, ] } @@ -595,8 +583,7 @@ fn sign_event_permission_for_kind(event_kind: u32) -> String { mod tests { use super::{ KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, - KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, KIND_PROFILE, cli_write_kinds, - myc_managed_account_ref_matches, myc_write_kind_readiness, + KIND_PROFILE, cli_write_kinds, myc_managed_account_ref_matches, myc_write_kind_readiness, myc_write_kind_readiness_for_permissions, sign_event_permission_for_kind, }; use radroots_nostr_connect::prelude::{ @@ -624,9 +611,6 @@ mod tests { "trade.accept", "trade.decline", "trade.cancel", - "trade.revision.propose", - "trade.revision.accept", - "trade.revision.decline", ] ); assert!(!commands.contains(&"signer.status.get")); @@ -657,27 +641,6 @@ mod tests { } #[test] - fn order_revision_readiness_uses_active_revision_kinds() { - let proposal = cli_write_kinds() - .into_iter() - .find(|kind| kind.command == "trade.revision.propose") - .expect("order revision propose readiness"); - - assert_eq!(proposal.event_kind, KIND_ORDER_REVISION_PROPOSAL); - assert_ne!(proposal.event_kind, RESERVED_ORDER_KIND_3431); - - for command in ["trade.revision.accept", "trade.revision.decline"] { - let write_kind = cli_write_kinds() - .into_iter() - .find(|kind| kind.command == command) - .expect("order revision decision readiness"); - - assert_eq!(write_kind.event_kind, KIND_ORDER_REVISION_DECISION); - assert_ne!(write_kind.event_kind, RESERVED_ORDER_KIND_3431); - } - } - - #[test] fn order_cancel_readiness_uses_order_cancellation_kind() { let cancel = cli_write_kinds() .into_iter() diff --git a/src/runtime/store.rs b/src/runtime/store.rs @@ -10,7 +10,7 @@ use radroots_sdk::{ SdkOutboxStorageStatus, SdkRestoreState, SdkSqliteStoreStatus, SdkStorageKind, StorageStatusReceipt, StorageStatusRequest, }; -use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::SqlxSqliteExecutor; use serde::Serialize; use serde_json::{Value, json}; @@ -30,13 +30,14 @@ const SDK_CANONICAL_SOURCE: &str = "SDK canonical event store and outbox"; const SDK_CANONICAL_STORE: &str = "sdk"; const SDK_BACKUP_KIND: &str = "sdk_canonical"; const SDK_BACKUP_MANIFEST_FILE: &str = "manifest.json"; -const SDK_EVENT_STORE_FILE: &str = "event_store.sqlite"; -const SDK_OUTBOX_FILE: &str = "outbox.sqlite"; +const SDK_RUNTIME_FILE: &str = "runtime.sqlite"; +const SDK_PRIVATE_FILE: &str = "private.sqlite"; +const SDK_STUDIO_FILE: &str = "studio.sqlite"; pub fn init(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> { let existed = config.local.replica_store_path.exists(); ensure_local_roots(config)?; - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; migrations::run_all_up(&executor)?; ensure_sync_run_table(&executor)?; let manifest = export_manifest(&executor)?; @@ -59,7 +60,7 @@ pub fn init(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> { pub fn init_preflight(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> { validate_local_roots(config)?; if config.local.replica_store_path.exists() { - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; ensure_sync_run_table(&executor)?; let manifest = export_manifest(&executor)?; return Ok(LocalInitView { @@ -129,7 +130,7 @@ fn derived_projection_status( }); } - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; ensure_sync_run_table(&executor)?; let manifest = export_manifest(&executor)?; let sync = radroots_replica_sync_status(&executor)?; @@ -178,8 +179,8 @@ pub fn backup_preflight( canonical_store: SDK_CANONICAL_STORE.to_owned(), destination: output.display().to_string(), file: output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string(), - event_store_file: Some(output.join(SDK_EVENT_STORE_FILE).display().to_string()), - outbox_file: Some(output.join(SDK_OUTBOX_FILE).display().to_string()), + event_store_file: Some(output.join(SDK_RUNTIME_FILE).display().to_string()), + outbox_file: Some(output.join(SDK_RUNTIME_FILE).display().to_string()), manifest_file: Some(output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string()), size_bytes: 0, manifest, @@ -232,7 +233,7 @@ pub fn export( ensure_safe_output_path(config, output)?; create_parent_dir(output)?; - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; let manifest = export_manifest(&executor)?; let sync = radroots_replica_sync_status(&executor)?; let records = match format { @@ -338,7 +339,9 @@ fn validate_directory_target(path: &Path) -> Result<(), RuntimeError> { } fn sdk_storage_files_exist(sdk_root: &Path) -> bool { - sdk_root.join(SDK_EVENT_STORE_FILE).exists() && sdk_root.join(SDK_OUTBOX_FILE).exists() + sdk_root.join(SDK_RUNTIME_FILE).exists() + && sdk_root.join(SDK_PRIVATE_FILE).exists() + && sdk_root.join(SDK_STUDIO_FILE).exists() } fn sdk_status_view( @@ -349,14 +352,10 @@ fn sdk_status_view( integrity: IntegrityReceipt, derived_projection: LocalDerivedProjectionStatusView, ) -> LocalStatusView { - let event_store_path = receipt + let runtime_path = receipt .paths .as_ref() - .map(|paths| paths.event_store_path.display().to_string()); - let outbox_path = receipt - .paths - .as_ref() - .map(|paths| paths.outbox_path.display().to_string()); + .map(|paths| paths.runtime_path.display().to_string()); let state = sdk_status_state(&receipt, &integrity).to_owned(); let reason = sdk_status_reason(&state); let actions = sdk_status_actions(&state); @@ -368,8 +367,8 @@ fn sdk_status_view( sdk_storage: sdk_storage_kind_label(receipt.storage).to_owned(), sdk_root: sdk_root.display().to_string(), sdk_existed_before_open, - event_store: sdk_event_store_status_view(receipt.event_store, event_store_path), - outbox: sdk_outbox_status_view(receipt.outbox, outbox_path), + event_store: sdk_event_store_status_view(receipt.event_store, runtime_path.clone()), + outbox: sdk_outbox_status_view(receipt.outbox, runtime_path), integrity: sdk_integrity_view(integrity), derived_projection, reason, @@ -430,7 +429,6 @@ fn sdk_outbox_status_view( retryable_events: status.retryable_events, terminal_events: status.terminal_events, failed_terminal_events: status.failed_terminal_events, - preview_unavailable_events: status.preview_unavailable_events, deferred_until_implemented_events: status.deferred_until_implemented_events, ready_signed_events: status.ready_signed_events, publishing_events: status.publishing_events, @@ -469,13 +467,15 @@ fn ensure_safe_sdk_backup_destination( output: &Path, ) -> Result<(), RuntimeError> { let sdk_root = sdk_storage_root(config); - let sdk_event_store_path = sdk_root.join(SDK_EVENT_STORE_FILE); - let sdk_outbox_path = sdk_root.join(SDK_OUTBOX_FILE); + let sdk_runtime_path = sdk_root.join(SDK_RUNTIME_FILE); + let sdk_private_path = sdk_root.join(SDK_PRIVATE_FILE); + let sdk_studio_path = sdk_root.join(SDK_STUDIO_FILE); let forbidden_paths = [ sdk_root.as_path(), config.local.replica_store_path.as_path(), - sdk_event_store_path.as_path(), - sdk_outbox_path.as_path(), + sdk_runtime_path.as_path(), + sdk_private_path.as_path(), + sdk_studio_path.as_path(), ]; if forbidden_paths.iter().any(|forbidden| output == *forbidden) { return Err(RuntimeError::Config(format!( @@ -497,13 +497,15 @@ fn ensure_safe_sdk_restore_destination( destination: &Path, ) -> Result<(), RuntimeError> { let sdk_root = sdk_storage_root(config); - let sdk_event_store_path = sdk_root.join(SDK_EVENT_STORE_FILE); - let sdk_outbox_path = sdk_root.join(SDK_OUTBOX_FILE); + let sdk_runtime_path = sdk_root.join(SDK_RUNTIME_FILE); + let sdk_private_path = sdk_root.join(SDK_PRIVATE_FILE); + let sdk_studio_path = sdk_root.join(SDK_STUDIO_FILE); let forbidden_paths = [ config.local.root.as_path(), config.local.replica_store_path.as_path(), - sdk_event_store_path.as_path(), - sdk_outbox_path.as_path(), + sdk_runtime_path.as_path(), + sdk_private_path.as_path(), + sdk_studio_path.as_path(), ]; if forbidden_paths .iter() @@ -527,11 +529,12 @@ fn ensure_safe_sdk_restore_destination( } fn sdk_backup_view(receipt: BackupReceipt) -> Result<LocalBackupView, CliSdkAdapterError> { - let event_store_file = receipt.event_store_path.as_ref().map(display_path); - let outbox_file = receipt.outbox_path.as_ref().map(display_path); + let event_store_file = receipt.runtime_path.as_ref().map(display_path); + let outbox_file = receipt.runtime_path.as_ref().map(display_path); let manifest_file = receipt.manifest_path.as_ref().map(display_path); - let size_bytes = path_size(receipt.event_store_path.as_ref())? - + path_size(receipt.outbox_path.as_ref())? + let size_bytes = path_size(receipt.runtime_path.as_ref())? + + path_size(receipt.private_path.as_ref())? + + path_size(receipt.studio_path.as_ref())? + path_size(receipt.manifest_path.as_ref())?; Ok(LocalBackupView { state: sdk_backup_state_label(receipt.state).to_owned(), @@ -570,15 +573,14 @@ fn sdk_restore_view( .as_ref() .map(display_path) .unwrap_or_default(), - event_store_file: display_path(&receipt.event_store_path), - outbox_file: display_path(&receipt.outbox_path), + event_store_file: display_path(&receipt.runtime_path), + outbox_file: display_path(&receipt.runtime_path), manifest_file: display_path(&receipt.manifest_path), destination_event_store_file: destination_paths - .map(|paths| display_path(&paths.event_store_path)), - destination_outbox_file: destination_paths.map(|paths| display_path(&paths.outbox_path)), - restored_event_store_file: restored_paths - .map(|paths| display_path(&paths.event_store_path)), - restored_outbox_file: restored_paths.map(|paths| display_path(&paths.outbox_path)), + .map(|paths| display_path(&paths.runtime_path)), + destination_outbox_file: destination_paths.map(|paths| display_path(&paths.runtime_path)), + restored_event_store_file: restored_paths.map(|paths| display_path(&paths.runtime_path)), + restored_outbox_file: restored_paths.map(|paths| display_path(&paths.runtime_path)), manifest: json_value(&receipt.manifest)?, verification: json_value(&receipt.verification)?, overwrite, @@ -616,8 +618,9 @@ fn sdk_backup_manifest_preview( "source_storage": sdk_storage_kind_label(status.storage), "source_paths": &status.paths, "backup_paths": { - "event_store_path": output.join(SDK_EVENT_STORE_FILE).display().to_string(), - "outbox_path": output.join(SDK_OUTBOX_FILE).display().to_string(), + "runtime_path": output.join(SDK_RUNTIME_FILE).display().to_string(), + "private_path": output.join(SDK_PRIVATE_FILE).display().to_string(), + "studio_path": output.join(SDK_STUDIO_FILE).display().to_string(), }, "source_status": status, "backup_verification": { diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -20,9 +20,10 @@ use radroots_sdk::{ PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind, PushOutboxTargetReceipt, SyncStatusReceipt, SyncStatusRequest, }; -use radroots_sql_core::{SqlExecutor, SqliteExecutor}; +use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor}; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportStatus, RadrootsTransportTarget, }; @@ -257,7 +258,7 @@ where let failed_transport_targets = relay_failures(receipt.failed_relays); let reason = relay_failure_reason(&failed_transport_targets); let failure_reason = format!("Nostr transport fetch failed: {reason}"); - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; migrations::run_all_up(&executor)?; record_sync_run( &executor, @@ -282,7 +283,7 @@ where Ok(receipt) => receipt, Err(error) => { let failure_reason = error.to_string(); - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; migrations::run_all_up(&executor)?; record_sync_run( &executor, @@ -305,7 +306,7 @@ where } }; - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; migrations::run_all_up(&executor)?; let ingest = ingest_events(&executor, &receipt, scope)?; record_sync_run( @@ -612,6 +613,8 @@ fn sdk_transport_status_view( endpoint_uri: status.endpoint_uri.clone(), configured: status.configured, implementation: status.implementation.clone(), + maturity: status.maturity.clone(), + availability: status.availability.clone(), usable_for_delivery: status.usable_for_delivery, capabilities: TransportOperationCapabilitiesView { deliver: status.capabilities.deliver, @@ -634,7 +637,7 @@ fn sdk_sync_status_actions(receipt: &SyncStatusReceipt) -> Vec<String> { fn sdk_sync_push_actions(state: &str, retryable: bool) -> Vec<String> { match state { - "published" | "ready" | "preview_unavailable" | "deferred_until_implemented" => { + "published" | "ready" | "deferred_until_implemented" => { vec!["radroots sync status get".to_owned()] } "dry_run" | "partial" | "unavailable" if retryable => { @@ -652,7 +655,6 @@ fn sdk_sync_push_reason_code(state: &str) -> Option<&'static str> { "dry_run" => Some("dry_run"), "partial" => Some("sdk_outbox_push_partial"), "unavailable" => Some("sdk_outbox_push_failed"), - "preview_unavailable" => Some("sdk_outbox_push_preview_unavailable"), "deferred_until_implemented" => Some("sdk_outbox_push_deferred_until_implemented"), _ => None, } @@ -660,7 +662,7 @@ fn sdk_sync_push_reason_code(state: &str) -> Option<&'static str> { fn sdk_push_state(receipt: &PushOutboxReceipt, failed_count: usize) -> &'static str { if receipt.attempted_events == 0 { - return sdk_push_reported_preview_state(receipt).unwrap_or("ready"); + return sdk_push_reported_deferred_state(receipt).unwrap_or("ready"); } if receipt.published_events > 0 && failed_count > 0 { "partial" @@ -673,19 +675,15 @@ fn sdk_push_state(receipt: &PushOutboxReceipt, failed_count: usize) -> &'static } } -fn sdk_push_reported_preview_state(receipt: &PushOutboxReceipt) -> Option<&'static str> { +fn sdk_push_reported_deferred_state(receipt: &PushOutboxReceipt) -> Option<&'static str> { let mut deferred = false; for event in &receipt.events { match event.final_state { - PushOutboxEventState::PreviewUnavailable => return Some("preview_unavailable"), PushOutboxEventState::DeferredUntilImplemented => deferred = true, _ => {} } for target in &event.targets { match target.outcome_kind { - PushOutboxTargetOutcomeKind::PreviewUnavailable => { - return Some("preview_unavailable"); - } PushOutboxTargetOutcomeKind::DeferredUntilImplemented => deferred = true, _ => {} } @@ -696,15 +694,12 @@ fn sdk_push_reported_preview_state(receipt: &PushOutboxReceipt) -> Option<&'stat fn sdk_push_reason(receipt: &PushOutboxReceipt, failed_count: usize) -> Option<String> { if receipt.attempted_events == 0 { - if let Some(state) = sdk_push_reported_preview_state(receipt) { + if let Some(state) = sdk_push_reported_deferred_state(receipt) { return Some(match state { - "preview_unavailable" => { - "SDK outbox push reported Reticulum preview work as preview unavailable without network delivery" - } "deferred_until_implemented" => { - "SDK outbox push reported Reticulum preview work as deferred until implemented without network delivery" + "SDK outbox push reported Reticulum work as deferred until implemented without network delivery" } - _ => "SDK outbox push reported Reticulum preview work without network delivery", + _ => "SDK outbox push reported Reticulum work without network delivery", } .to_owned()); } @@ -738,7 +733,6 @@ fn sdk_sync_queue(receipt: &SyncStatusReceipt) -> SyncQueueView { retryable_count: Some(usize_from_i64(receipt.outbox.retryable_events)), terminal_count: Some(usize_from_i64(receipt.outbox.terminal_events)), failed_terminal_count: Some(usize_from_i64(receipt.outbox.failed_terminal_events)), - preview_unavailable_count: Some(usize_from_i64(receipt.outbox.preview_unavailable_events)), deferred_until_implemented_count: Some(usize_from_i64( receipt.outbox.deferred_until_implemented_events, )), @@ -757,7 +751,6 @@ fn derived_projection_sync_queue(expected_count: usize, pending_count: usize) -> retryable_count: None, terminal_count: None, failed_terminal_count: None, - preview_unavailable_count: None, deferred_until_implemented_count: None, ready_signed_count: None, publishing_count: None, @@ -887,13 +880,13 @@ fn sync_configured_transport_targets( )?); } } - TransportProfileKind::ReticulumPreview => { + TransportProfileKind::Reticulum => { targets.push(sync_transport_target_view( RadrootsTransportKind::Reticulum, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, + RADROOTS_RETICULUM_ENDPOINT_URI, )?); } - TransportProfileKind::Hybrid => { + TransportProfileKind::MultiTarget => { for relay_url in &config.transport.nostr_relay_urls { targets.push(sync_transport_target_view( RadrootsTransportKind::Nostr, @@ -902,13 +895,7 @@ fn sync_configured_transport_targets( } targets.push(sync_transport_target_view( RadrootsTransportKind::Reticulum, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, - )?); - } - TransportProfileKind::Proxy => { - targets.push(sync_transport_target_view( - RadrootsTransportKind::Proxy, - config.transport.proxy.url.as_str(), + RADROOTS_RETICULUM_ENDPOINT_URI, )?); } } @@ -922,17 +909,9 @@ fn sync_transport_target_view( let target = match kind { RadrootsTransportKind::Nostr => RadrootsTransportTarget::nostr_relay(endpoint_uri), RadrootsTransportKind::Reticulum => { - if endpoint_uri != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI { - Err(radroots_transport::RadrootsTransportError::InvalidTargetUri) - } else { - RadrootsTransportTarget::reticulum_preview() - } + RadrootsTransportTarget::reticulum_with_metadata(endpoint_uri, None, None) } RadrootsTransportKind::Local => RadrootsTransportTarget::local(endpoint_uri), - RadrootsTransportKind::Proxy => RadrootsTransportTarget::proxy(endpoint_uri), - RadrootsTransportKind::Mesh | RadrootsTransportKind::Custom(_) => { - RadrootsTransportTarget::new(kind, endpoint_uri) - } } .map_err(|error| RuntimeError::Config(format!("invalid transport target: {error}")))?; Ok(SyncTransportTargetView { @@ -964,37 +943,18 @@ fn sync_transport_statuses(config: &RuntimeConfig) -> Vec<SyncTransportStatusVie nostr_targets_configured, ))] } - TransportProfileKind::ReticulumPreview => { - vec![sync_transport_status_view( - reticulum_preview_transport_status(profile_id), - )] + TransportProfileKind::Reticulum => { + vec![sync_transport_status_view(reticulum_transport_status( + profile_id, + ))] } - TransportProfileKind::Hybrid => vec![ + TransportProfileKind::MultiTarget => vec![ sync_transport_status_view(nostr_transport_status( profile_id, nostr_targets_configured, )), - sync_transport_status_view(reticulum_preview_transport_status(profile_id)), + sync_transport_status_view(reticulum_transport_status(profile_id)), ], - TransportProfileKind::Proxy => { - let auth_configured = config.transport.proxy.token_file.is_some() - || config.transport.proxy.token_secret_id.is_some(); - vec![sync_transport_status_view( - RadrootsTransportStatus::new( - RadrootsTransportKind::Proxy, - auth_configured, - RadrootsTransportImplementationState::Real, - auth_configured, - if auth_configured { - "Proxy transport delegates delivery to the configured endpoint" - } else { - "Proxy transport requires a configured token file or token secret id" - }, - ) - .with_profile_id(profile_id) - .with_endpoint_uri(config.transport.proxy.url.as_str()), - )] - } } } @@ -1013,16 +973,18 @@ fn nostr_transport_status(profile_id: &str, targets_configured: bool) -> Radroot .with_profile_id(profile_id) } -fn reticulum_preview_transport_status(profile_id: &str) -> RadrootsTransportStatus { +fn reticulum_transport_status(profile_id: &str) -> RadrootsTransportStatus { RadrootsTransportStatus::new( RadrootsTransportKind::Reticulum, true, - RadrootsTransportImplementationState::PreviewUnavailable, + RadrootsTransportImplementationState::Real, false, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, ) .with_profile_id(profile_id) - .with_endpoint_uri(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI) + .with_endpoint_uri(RADROOTS_RETICULUM_ENDPOINT_URI) + .with_maturity(RadrootsTransportCapabilityMaturity::Preview) + .with_availability(RadrootsTransportCapabilityAvailability::Unavailable) } fn sync_transport_status_view(status: RadrootsTransportStatus) -> SyncTransportStatusView { @@ -1032,6 +994,8 @@ fn sync_transport_status_view(status: RadrootsTransportStatus) -> SyncTransportS endpoint_uri: status.endpoint_uri, configured: status.configured, implementation: transport_implementation_label(status.implementation).to_owned(), + maturity: transport_maturity_label(status.maturity).to_owned(), + availability: transport_availability_label(status.availability).to_owned(), usable_for_delivery: status.usable_for_delivery, capabilities: TransportOperationCapabilitiesView { deliver: status.capabilities.deliver, @@ -1045,7 +1009,23 @@ fn transport_implementation_label(state: RadrootsTransportImplementationState) - match state { RadrootsTransportImplementationState::Real => "real", RadrootsTransportImplementationState::Mock => "mock", - RadrootsTransportImplementationState::PreviewUnavailable => "preview_unavailable", + } +} + +fn transport_maturity_label(maturity: RadrootsTransportCapabilityMaturity) -> &'static str { + match maturity { + RadrootsTransportCapabilityMaturity::Preview => "preview", + RadrootsTransportCapabilityMaturity::Stable => "stable", + } +} + +fn transport_availability_label( + availability: RadrootsTransportCapabilityAvailability, +) -> &'static str { + match availability { + RadrootsTransportCapabilityAvailability::Available => "available", + RadrootsTransportCapabilityAvailability::Degraded => "degraded", + RadrootsTransportCapabilityAvailability::Unavailable => "unavailable", } } @@ -1068,7 +1048,7 @@ fn inspect_sync(config: &RuntimeConfig) -> Result<SyncSnapshot, RuntimeError> { }); } - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; migrations::run_all_up(&executor)?; let queue = radroots_replica_sync_status(&executor)?; let freshness = @@ -1147,7 +1127,7 @@ pub(crate) fn freshness_for_scope( config: &RuntimeConfig, scope: RelayIngestScope, ) -> Result<SyncFreshnessView, RuntimeError> { - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; migrations::run_all_up(&executor)?; freshness_for_scope_from_executor(config, &executor, scope) } @@ -1159,7 +1139,7 @@ pub(crate) fn relay_provenance_relays_for_scope( if !config.local.replica_store_path.exists() { return Ok(Vec::new()); } - let executor = SqliteExecutor::open(&config.local.replica_store_path)?; + let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?; migrations::run_all_up(&executor)?; ensure_sync_run_table(&executor)?; let current_fingerprint = relay_set_fingerprint(&config.transport.nostr_relay_urls); @@ -1178,7 +1158,7 @@ pub(crate) fn relay_provenance_relays_for_scope( pub(crate) fn freshness_for_scope_from_executor( config: &RuntimeConfig, - executor: &SqliteExecutor, + executor: &SqlxSqliteExecutor, scope: RelayIngestScope, ) -> Result<SyncFreshnessView, RuntimeError> { let last_event_at = ReplicaSql::new(executor).nostr_event_last_created_at()?; @@ -1305,7 +1285,7 @@ fn sync_run_freshness_view( } } -pub(crate) fn ensure_sync_run_table(executor: &SqliteExecutor) -> Result<(), RuntimeError> { +pub(crate) fn ensure_sync_run_table(executor: &SqlxSqliteExecutor) -> Result<(), RuntimeError> { executor.exec( "CREATE TABLE IF NOT EXISTS radroots_cli_sync_run ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1332,7 +1312,7 @@ pub(crate) fn ensure_sync_run_table(executor: &SqliteExecutor) -> Result<(), Run } fn latest_sync_run( - executor: &SqliteExecutor, + executor: &SqlxSqliteExecutor, scope: RelayIngestScope, ) -> Result<Option<SyncRunRecord>, RuntimeError> { let rows = executor.query_raw( @@ -1381,7 +1361,10 @@ fn sync_run_record_from_row(row: SyncRunRow) -> SyncRunRecord { } } -fn record_sync_run(executor: &SqliteExecutor, record: &SyncRunRecord) -> Result<(), RuntimeError> { +fn record_sync_run( + executor: &SqlxSqliteExecutor, + record: &SyncRunRecord, +) -> Result<(), RuntimeError> { ensure_sync_run_table(executor)?; executor.exec( &format!( @@ -1657,7 +1640,7 @@ impl RelayIngestScope { } fn ingest_events( - executor: &SqliteExecutor, + executor: &SqlxSqliteExecutor, receipt: &RadrootsRelayFetchedEventsReceipt, scope: RelayIngestScope, ) -> Result<RelayIngestCounts, RuntimeError> { @@ -1801,7 +1784,7 @@ mod tests { }; use radroots_secret_vault::RadrootsSecretBackend; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, }; use radroots_transport_nostr::{ RadrootsRelayFetchFailure, RadrootsRelayFetchedEvent, RadrootsRelayFetchedEventsReceipt, @@ -1818,7 +1801,7 @@ mod tests { use crate::runtime::config::{ AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, - ReticulumPreviewBehavior, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, + ReticulumBehavior, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, TransportProfileKind, Verbosity, }; @@ -1903,12 +1886,13 @@ mod tests { } #[test] - fn sync_pull_hybrid_reports_profile_targets_and_nostr_fetch_detail() { + fn sync_pull_multi_target_reports_profile_targets_and_nostr_fetch_detail() { let dir = tempdir().expect("tempdir"); - let config = hybrid_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); + let config = multi_target_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); crate::runtime::store::init(&config).expect("store init"); - let view = pull_with_fetcher(&config, fake_fetcher(Vec::new())).expect("sync pull hybrid"); + let view = + pull_with_fetcher(&config, fake_fetcher(Vec::new())).expect("sync pull multi_target"); assert_eq!(view.state, "ready"); assert_eq!(view.configured_transport_target_count, 2); @@ -1929,10 +1913,9 @@ mod tests { assert!(!view.transport_statuses[0].capabilities.fetch); assert_eq!(view.transport_statuses[1].transport, "reticulum"); assert!(view.transport_statuses[1].configured); - assert_eq!( - view.transport_statuses[1].implementation, - "preview_unavailable" - ); + assert_eq!(view.transport_statuses[1].implementation, "real"); + assert_eq!(view.transport_statuses[1].maturity, "preview"); + assert_eq!(view.transport_statuses[1].availability, "unavailable"); assert!(!view.transport_statuses[1].usable_for_delivery); assert!(!view.transport_statuses[1].capabilities.deliver); assert!(!view.transport_statuses[1].capabilities.fetch); @@ -1947,15 +1930,15 @@ mod tests { } #[test] - fn sync_pull_empty_hybrid_profile_reports_nostr_misconfigured() { + fn sync_pull_empty_multi_target_profile_reports_nostr_misconfigured() { let dir = tempdir().expect("tempdir"); - let config = hybrid_config(dir.path(), Vec::new()); + let config = multi_target_config(dir.path(), Vec::new()); crate::runtime::store::init(&config).expect("store init"); let view = pull_with_fetcher(&config, |_, _| { - panic!("empty Hybrid profile sync pull must not fetch") + panic!("empty MultiTarget profile sync pull must not fetch") }) - .expect("sync pull empty Hybrid profile"); + .expect("sync pull empty MultiTarget profile"); assert_eq!(view.state, "unavailable"); assert_eq!(view.configured_transport_target_count, 1); @@ -1968,7 +1951,7 @@ mod tests { assert_eq!(view.transport_statuses[0].transport, "nostr"); assert_eq!( view.transport_statuses[0].profile_id.as_deref(), - Some("hybrid") + Some("multi_target") ); assert!(!view.transport_statuses[0].configured); assert_eq!(view.transport_statuses[0].implementation, "real"); @@ -1977,10 +1960,9 @@ mod tests { assert!(!view.transport_statuses[0].capabilities.fetch); assert_eq!(view.transport_statuses[1].transport, "reticulum"); assert!(view.transport_statuses[1].configured); - assert_eq!( - view.transport_statuses[1].implementation, - "preview_unavailable" - ); + assert_eq!(view.transport_statuses[1].implementation, "real"); + assert_eq!(view.transport_statuses[1].maturity, "preview"); + assert_eq!(view.transport_statuses[1].availability, "unavailable"); assert!(!view.transport_statuses[1].usable_for_delivery); assert!(!view.transport_statuses[1].capabilities.deliver); assert!(!view.transport_statuses[1].capabilities.fetch); @@ -1989,34 +1971,33 @@ mod tests { } #[test] - fn sync_inspect_hybrid_uses_profile_target_count() { + fn sync_inspect_multi_target_uses_profile_target_count() { let dir = tempdir().expect("tempdir"); - let config = hybrid_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); + let config = multi_target_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); crate::runtime::store::init(&config).expect("store init"); - let snapshot = inspect_sync(&config).expect("inspect sync hybrid"); + let snapshot = inspect_sync(&config).expect("inspect sync multi_target"); assert_eq!(snapshot.state, "ready"); assert_eq!(snapshot.configured_transport_target_count, 2); assert_eq!(snapshot.configured_transport_targets.len(), 2); assert_eq!(snapshot.transport_statuses.len(), 2); assert_eq!(snapshot.transport_statuses[1].transport, "reticulum"); - assert_eq!( - snapshot.transport_statuses[1].implementation, - "preview_unavailable" - ); + assert_eq!(snapshot.transport_statuses[1].implementation, "real"); + assert_eq!(snapshot.transport_statuses[1].maturity, "preview"); + assert_eq!(snapshot.transport_statuses[1].availability, "unavailable"); } #[test] - fn sync_pull_reticulum_preview_does_not_report_fetch_success() { + fn sync_pull_reticulum_does_not_report_fetch_success() { let dir = tempdir().expect("tempdir"); - let config = reticulum_preview_config(dir.path()); + let config = reticulum_config(dir.path()); crate::runtime::store::init(&config).expect("store init"); let view = pull_with_fetcher(&config, |_, _| { - panic!("reticulum preview sync pull must not run Nostr fetch") + panic!("reticulum sync pull must not run Nostr fetch") }) - .expect("sync pull reticulum preview"); + .expect("sync pull reticulum"); assert_eq!(view.state, "unavailable"); assert_eq!(view.configured_transport_target_count, 1); @@ -2025,10 +2006,9 @@ mod tests { "reticulum" ); assert!(view.transport_statuses[0].configured); - assert_eq!( - view.transport_statuses[0].implementation, - "preview_unavailable" - ); + assert_eq!(view.transport_statuses[0].implementation, "real"); + assert_eq!(view.transport_statuses[0].maturity, "preview"); + assert_eq!(view.transport_statuses[0].availability, "unavailable"); assert!(!view.transport_statuses[0].usable_for_delivery); assert!(!view.transport_statuses[0].capabilities.deliver); assert!(!view.transport_statuses[0].capabilities.fetch); @@ -2037,7 +2017,7 @@ mod tests { assert!( view.reason .as_deref() - .expect("reticulum preview reason") + .expect("reticulum unavailable reason") .contains("does not expose Nostr relay fetch targets") ); } @@ -2065,7 +2045,6 @@ mod tests { 0, 0, 0, - 0, None, None, &["wss://relay-a.example.com", "wss://relay-b.example.com"], @@ -2080,16 +2059,15 @@ mod tests { assert_eq!(view.queue.pending_count, 0); assert_eq!(view.queue.retryable_count, Some(0)); assert_eq!(view.queue.terminal_count, Some(0)); - assert_eq!(view.queue.preview_unavailable_count, Some(0)); assert_eq!(view.queue.deferred_until_implemented_count, Some(0)); assert_eq!(view.queue.ready_signed_count, Some(0)); assert_eq!(view.actions, vec!["radroots sync pull"]); } #[test] - fn sync_status_hybrid_reports_profile_targets_and_statuses() { + fn sync_status_multi_target_reports_profile_targets_and_statuses() { let dir = tempdir().expect("tempdir"); - let config = hybrid_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); + let config = multi_target_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); let mut receipt = sdk_status_receipt( 0, 0, @@ -2100,13 +2078,12 @@ mod tests { 0, 0, 0, - 0, None, None, &["wss://relay.example.com"], ); receipt.transport_profile = SyncTransportProfileSummary { - transport_profile_id: "hybrid".to_owned(), + transport_profile_id: "multi_target".to_owned(), configured_transport_target_count: 2, configured_transport_targets: vec![ SyncTransportTargetSummary { @@ -2118,19 +2095,21 @@ mod tests { }, SyncTransportTargetSummary { transport_kind: "reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), endpoint_fingerprint: "1".repeat(64), - target_scope: Some("local_preview".to_owned()), + target_scope: Some("local".to_owned()), target_label: None, }, ], transport_statuses: vec![ SyncTransportStatusSummary { transport: "nostr".to_owned(), - profile_id: Some("hybrid".to_owned()), + profile_id: Some("multi_target".to_owned()), endpoint_uri: None, configured: true, implementation: "real".to_owned(), + maturity: "stable".to_owned(), + availability: "available".to_owned(), usable_for_delivery: true, capabilities: SyncTransportOperationCapabilitiesSummary { deliver: true, @@ -2140,10 +2119,12 @@ mod tests { }, SyncTransportStatusSummary { transport: "reticulum".to_owned(), - profile_id: Some("hybrid".to_owned()), - endpoint_uri: Some(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned()), + profile_id: Some("multi_target".to_owned()), + endpoint_uri: Some(RADROOTS_RETICULUM_ENDPOINT_URI.to_owned()), configured: true, - implementation: "preview_unavailable".to_owned(), + implementation: "real".to_owned(), + maturity: "preview".to_owned(), + availability: "unavailable".to_owned(), usable_for_delivery: false, capabilities: SyncTransportOperationCapabilitiesSummary { deliver: false, @@ -2164,10 +2145,9 @@ mod tests { "reticulum" ); assert_eq!(view.transport_statuses.len(), 2); - assert_eq!( - view.transport_statuses[1].implementation, - "preview_unavailable" - ); + assert_eq!(view.transport_statuses[1].implementation, "real"); + assert_eq!(view.transport_statuses[1].maturity, "preview"); + assert_eq!(view.transport_statuses[1].availability, "unavailable"); assert!(!view.transport_statuses[1].usable_for_delivery); assert!(!view.transport_statuses[1].capabilities.deliver); assert!(!view.transport_statuses[1].capabilities.fetch); @@ -2188,7 +2168,6 @@ mod tests { 2, 1, 0, - 0, 1, 0, Some(1_700_000_010_000), @@ -2203,7 +2182,6 @@ mod tests { assert_eq!(view.queue.retryable_count, Some(1)); assert_eq!(view.queue.terminal_count, Some(2)); assert_eq!(view.queue.failed_terminal_count, Some(1)); - assert_eq!(view.queue.preview_unavailable_count, Some(0)); assert_eq!(view.queue.deferred_until_implemented_count, Some(0)); assert_eq!(view.queue.ready_signed_count, Some(1)); assert_eq!(view.queue.last_attempt_at_ms, Some(1_700_000_010_000)); @@ -2230,7 +2208,6 @@ mod tests { 0, 0, 0, - 0, 1, 0, None, @@ -2279,7 +2256,6 @@ mod tests { 0, 0, 0, - 0, None, None, &["wss://relay.example.com"], @@ -2298,15 +2274,14 @@ mod tests { } #[test] - fn sync_push_reticulum_preview_reports_non_attempted_preview_work() { + fn sync_push_reticulum_reports_non_attempted_deferred_work() { let cases = [ ( - PushOutboxEventState::PreviewUnavailable, - PushOutboxTargetOutcomeKind::PreviewUnavailable, - "preview_unavailable", - "sdk_outbox_push_preview_unavailable", - "SDK outbox push reported Reticulum preview work as preview unavailable without network delivery", - 1, + PushOutboxEventState::DeferredUntilImplemented, + PushOutboxTargetOutcomeKind::DeferredUntilImplemented, + "deferred_until_implemented", + "sdk_outbox_push_deferred_until_implemented", + "SDK outbox push reported Reticulum work as deferred until implemented without network delivery", 0, ), ( @@ -2314,8 +2289,7 @@ mod tests { PushOutboxTargetOutcomeKind::DeferredUntilImplemented, "deferred_until_implemented", "sdk_outbox_push_deferred_until_implemented", - "SDK outbox push reported Reticulum preview work as deferred until implemented without network delivery", - 0, + "SDK outbox push reported Reticulum work as deferred until implemented without network delivery", 1, ), ]; @@ -2326,24 +2300,23 @@ mod tests { expected_state, expected_reason_code, expected_reason, - preview_count, deferred_count, ) in cases { let dir = tempdir().expect("tempdir"); - let config = reticulum_preview_config(dir.path()); + let config = reticulum_config(dir.path()); let receipt = PushOutboxReceipt { attempted_events: 0, published_events: 0, retryable_events: 0, terminal_events: 0, - events: vec![sdk_reticulum_preview_push_event(final_state, outcome_kind)], + events: vec![sdk_reticulum_push_event(final_state, outcome_kind)], }; let view = sdk_push_view( &config, receipt, - sdk_reticulum_preview_status_receipt(preview_count, deferred_count), + sdk_reticulum_status_receipt(deferred_count), ); assert_eq!(view.state, expected_state); @@ -2354,7 +2327,7 @@ mod tests { assert_eq!(view.reason.as_deref(), Some(expected_reason)); assert_eq!( view.target_transport_endpoints, - vec![RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned()] + vec![RADROOTS_RETICULUM_ENDPOINT_URI.to_owned()] ); assert!(view.attempted_transport_endpoints.is_empty()); assert!(view.accepted_transport_endpoints.is_empty()); @@ -2362,7 +2335,7 @@ mod tests { assert_eq!(view.failed_transport_targets[0].transport_kind, "reticulum"); assert_eq!( view.failed_transport_targets[0].endpoint_uri, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI + RADROOTS_RETICULUM_ENDPOINT_URI ); assert_eq!( view.failed_transport_targets[0].reason, @@ -2418,7 +2391,6 @@ mod tests { 0, 0, 0, - 0, Some(1_700_000_020_000), Some("auth-required: login".to_owned()), &["wss://relay-a.example.com", "wss://relay-b.example.com"], @@ -2471,7 +2443,6 @@ mod tests { retryable_events: i64, terminal_events: i64, failed_terminal_events: i64, - preview_unavailable_events: i64, deferred_until_implemented_events: i64, ready_signed_events: i64, publishing_events: i64, @@ -2499,7 +2470,6 @@ mod tests { retryable_events, terminal_events, failed_terminal_events, - preview_unavailable_events, deferred_until_implemented_events, ready_signed_events, publishing_events, @@ -2526,6 +2496,8 @@ mod tests { endpoint_uri: None, configured: true, implementation: "real".to_owned(), + maturity: "stable".to_owned(), + availability: "available".to_owned(), usable_for_delivery: true, capabilities: SyncTransportOperationCapabilitiesSummary { deliver: true, @@ -2537,10 +2509,7 @@ mod tests { } } - fn sdk_reticulum_preview_status_receipt( - preview_unavailable_events: i64, - deferred_until_implemented_events: i64, - ) -> SyncStatusReceipt { + fn sdk_reticulum_status_receipt(deferred_until_implemented_events: i64) -> SyncStatusReceipt { SyncStatusReceipt { source: SyncStatusSource::SdkCanonicalStores, observed_at_ms: 1_700_000_030_000, @@ -2557,7 +2526,6 @@ mod tests { retryable_events: 0, terminal_events: 0, failed_terminal_events: 0, - preview_unavailable_events, deferred_until_implemented_events, ready_signed_events: 0, publishing_events: 0, @@ -2565,21 +2533,23 @@ mod tests { last_error: None, }, transport_profile: SyncTransportProfileSummary { - transport_profile_id: "reticulum_preview".to_owned(), + transport_profile_id: "reticulum".to_owned(), configured_transport_target_count: 1, configured_transport_targets: vec![SyncTransportTargetSummary { transport_kind: "reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), endpoint_fingerprint: "1".repeat(64), - target_scope: Some("local_preview".to_owned()), + target_scope: Some("local".to_owned()), target_label: None, }], transport_statuses: vec![SyncTransportStatusSummary { transport: "reticulum".to_owned(), - profile_id: Some("reticulum_preview".to_owned()), - endpoint_uri: Some(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned()), + profile_id: Some("reticulum".to_owned()), + endpoint_uri: Some(RADROOTS_RETICULUM_ENDPOINT_URI.to_owned()), configured: true, - implementation: "preview_unavailable".to_owned(), + implementation: "real".to_owned(), + maturity: "preview".to_owned(), + availability: "unavailable".to_owned(), usable_for_delivery: false, capabilities: SyncTransportOperationCapabilitiesSummary { deliver: false, @@ -2644,7 +2614,7 @@ mod tests { } } - fn sdk_reticulum_preview_push_event( + fn sdk_reticulum_push_event( final_state: PushOutboxEventState, outcome_kind: PushOutboxTargetOutcomeKind, ) -> PushOutboxEventReceipt { @@ -2660,8 +2630,8 @@ mod tests { quorum_met: false, targets: vec![PushOutboxTargetReceipt { transport_kind: "reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), - target_scope: Some("local_preview".to_owned()), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), + target_scope: Some("local".to_owned()), target_label: None, outcome_kind, transport_outcome_kind: test_transport_outcome_kind(outcome_kind), @@ -2686,9 +2656,6 @@ mod tests { PushOutboxTargetOutcomeKind::ConnectionFailed => { PushOutboxTransportOutcomeKind::ConnectionFailed } - PushOutboxTargetOutcomeKind::PreviewUnavailable => { - PushOutboxTransportOutcomeKind::TransportUnavailable - } PushOutboxTargetOutcomeKind::AuthRequired | PushOutboxTargetOutcomeKind::Blocked | PushOutboxTargetOutcomeKind::RateLimited @@ -3256,10 +3223,10 @@ mod tests { } } - fn hybrid_config(root: &Path, relays: Vec<String>) -> RuntimeConfig { + fn multi_target_config(root: &Path, relays: Vec<String>) -> RuntimeConfig { let mut config = sample_config(root, relays); - config.transport.profile = TransportProfileKind::Hybrid; - config.transport.reticulum_preview_behavior = ReticulumPreviewBehavior::DeferDeliveryPlans; + config.transport.profile = TransportProfileKind::MultiTarget; + config.transport.reticulum_behavior = ReticulumBehavior::DeferDeliveryPlans; config } @@ -3269,10 +3236,10 @@ mod tests { config } - fn reticulum_preview_config(root: &Path) -> RuntimeConfig { + fn reticulum_config(root: &Path) -> RuntimeConfig { let mut config = sample_config(root, Vec::new()); - config.transport.profile = TransportProfileKind::ReticulumPreview; - config.transport.reticulum_preview_behavior = ReticulumPreviewBehavior::DeferDeliveryPlans; + config.transport.profile = TransportProfileKind::Reticulum; + config.transport.reticulum_behavior = ReticulumBehavior::DeferDeliveryPlans; config } } diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs @@ -1,16 +1,15 @@ -use std::fs; -use std::path::PathBuf; - use radroots_sdk::{ PushOutboxEventState, PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind, SyncStatusRequest, }; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, - RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportImplementationState, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, + RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportCapabilityAvailability, + RadrootsTransportCapabilityMaturity, RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportStatus, }; use serde_json::Value as JsonValue; +use std::fs; use toml::{Value, map::Map}; use crate::ops::OperationData; @@ -51,80 +50,50 @@ pub fn set_profile( ); transport.insert("nostr".to_owned(), Value::Table(nostr)); } - "reticulum_preview" => { - let behavior = string_input(input, "reticulum_preview_behavior") - .unwrap_or("reject_delivery_attempts"); - let scope = string_input(input, "reticulum_preview_scope") - .unwrap_or(RADROOTS_RETICULUM_PREVIEW_SCOPE_ID); - let mut preview = Map::new(); - preview.insert("behavior".to_owned(), Value::String(behavior.to_owned())); - preview.insert("scope".to_owned(), Value::String(scope.to_owned())); - if let Some(agent_endpoint) = string_input(input, "reticulum_preview_agent_endpoint") { - preview.insert( + "reticulum" => { + let behavior = + string_input(input, "reticulum_behavior").unwrap_or("reject_delivery_attempts"); + let scope = + string_input(input, "reticulum_scope").unwrap_or(RADROOTS_RETICULUM_SCOPE_ID); + let mut reticulum = Map::new(); + reticulum.insert("behavior".to_owned(), Value::String(behavior.to_owned())); + reticulum.insert("scope".to_owned(), Value::String(scope.to_owned())); + if let Some(agent_endpoint) = string_input(input, "reticulum_agent_endpoint") { + reticulum.insert( "agent_endpoint".to_owned(), Value::String(agent_endpoint.to_owned()), ); } - transport.insert("reticulum_preview".to_owned(), Value::Table(preview)); + transport.insert("reticulum".to_owned(), Value::Table(reticulum)); } - "hybrid" => { + "multi_target" => { let relays = string_array_input(input, "nostr_relays"); if relays.is_empty() { return Err(RuntimeError::Config( - "transport profile `hybrid` requires at least one --nostr-relay".to_owned(), + "transport profile `multi_target` requires at least one --nostr-relay" + .to_owned(), )); } - let behavior = string_input(input, "reticulum_preview_behavior") - .unwrap_or("reject_delivery_attempts"); - let scope = string_input(input, "reticulum_preview_scope") - .unwrap_or(RADROOTS_RETICULUM_PREVIEW_SCOPE_ID); + let behavior = + string_input(input, "reticulum_behavior").unwrap_or("reject_delivery_attempts"); + let scope = + string_input(input, "reticulum_scope").unwrap_or(RADROOTS_RETICULUM_SCOPE_ID); let mut nostr = Map::new(); nostr.insert( "relay_urls".to_owned(), Value::Array(relays.into_iter().map(Value::String).collect()), ); - let mut preview = Map::new(); - preview.insert("behavior".to_owned(), Value::String(behavior.to_owned())); - preview.insert("scope".to_owned(), Value::String(scope.to_owned())); - if let Some(agent_endpoint) = string_input(input, "reticulum_preview_agent_endpoint") { - preview.insert( + let mut reticulum = Map::new(); + reticulum.insert("behavior".to_owned(), Value::String(behavior.to_owned())); + reticulum.insert("scope".to_owned(), Value::String(scope.to_owned())); + if let Some(agent_endpoint) = string_input(input, "reticulum_agent_endpoint") { + reticulum.insert( "agent_endpoint".to_owned(), Value::String(agent_endpoint.to_owned()), ); } transport.insert("nostr".to_owned(), Value::Table(nostr)); - transport.insert("reticulum_preview".to_owned(), Value::Table(preview)); - } - "proxy" => { - let Some(url) = string_input(input, "proxy_url") else { - return Err(RuntimeError::Config( - "transport profile `proxy` requires --proxy-url".to_owned(), - )); - }; - let token_file = string_input(input, "proxy_token_file").map(str::to_owned); - let token_secret_id = string_input(input, "proxy_token_secret_id").map(str::to_owned); - validate_proxy_token_source(token_file.as_deref(), token_secret_id.as_deref())?; - validate_proxy_token_material( - config, - url, - token_file.as_deref(), - token_secret_id.as_deref(), - )?; - let mut proxy = Map::new(); - proxy.insert("url".to_owned(), Value::String(url.to_owned())); - if let Some(token_file) = token_file.as_ref() { - proxy.insert( - "token_file".to_owned(), - Value::String(token_file.to_owned()), - ); - } - if let Some(token_secret_id) = token_secret_id.as_ref() { - proxy.insert( - "token_secret_id".to_owned(), - Value::String(token_secret_id.to_owned()), - ); - } - transport.insert("proxy".to_owned(), Value::Table(proxy)); + transport.insert("reticulum".to_owned(), Value::Table(reticulum)); } other => { return Err(RuntimeError::Config(format!( @@ -136,12 +105,9 @@ pub fn set_profile( Ok(profile_view_from_parts( kind, string_array_input(input, "nostr_relays"), - string_input(input, "reticulum_preview_behavior").map(str::to_owned), - reticulum_preview_scope_for_profile(kind, input), - string_input(input, "reticulum_preview_agent_endpoint").map(str::to_owned), - string_input(input, "proxy_url").map(str::to_owned), - string_input(input, "proxy_token_file").map(str::to_owned), - string_input(input, "proxy_token_secret_id").map(str::to_owned), + string_input(input, "reticulum_behavior").map(str::to_owned), + reticulum_scope_for_profile(kind, input), + string_input(input, "reticulum_agent_endpoint").map(str::to_owned), "configured", )) } @@ -180,7 +146,6 @@ pub fn outbox_status( pending_count: receipt.outbox.pending_events, retryable_count: receipt.outbox.retryable_events, terminal_count: receipt.outbox.terminal_events, - preview_unavailable_count: receipt.outbox.preview_unavailable_events, deferred_until_implemented_count: receipt.outbox.deferred_until_implemented_events, ready_signed_count: receipt.outbox.ready_signed_events, publishing_count: receipt.outbox.publishing_events, @@ -231,7 +196,7 @@ pub fn outbox_push(config: &RuntimeConfig) -> Result<TransportOutboxPushView, Cl fn transport_outbox_push_state(receipt: &PushOutboxReceipt, failed_count: usize) -> &'static str { if receipt.attempted_events == 0 { - return transport_outbox_reported_preview_state(receipt).unwrap_or("ready"); + return transport_outbox_reported_deferred_state(receipt).unwrap_or("ready"); } if receipt.published_events > 0 && failed_count > 0 { "partial" @@ -244,19 +209,15 @@ fn transport_outbox_push_state(receipt: &PushOutboxReceipt, failed_count: usize) } } -fn transport_outbox_reported_preview_state(receipt: &PushOutboxReceipt) -> Option<&'static str> { +fn transport_outbox_reported_deferred_state(receipt: &PushOutboxReceipt) -> Option<&'static str> { let mut deferred = false; for event in &receipt.events { match event.final_state { - PushOutboxEventState::PreviewUnavailable => return Some("preview_unavailable"), PushOutboxEventState::DeferredUntilImplemented => deferred = true, _ => {} } for target in &event.targets { match target.outcome_kind { - PushOutboxTargetOutcomeKind::PreviewUnavailable => { - return Some("preview_unavailable"); - } PushOutboxTargetOutcomeKind::DeferredUntilImplemented => deferred = true, _ => {} } @@ -267,15 +228,12 @@ fn transport_outbox_reported_preview_state(receipt: &PushOutboxReceipt) -> Optio fn transport_outbox_push_reason(receipt: &PushOutboxReceipt) -> Option<String> { if receipt.attempted_events == 0 { - if let Some(state) = transport_outbox_reported_preview_state(receipt) { + if let Some(state) = transport_outbox_reported_deferred_state(receipt) { return Some(match state { - "preview_unavailable" => { - "SDK outbox push reported Reticulum preview work as preview unavailable without network delivery" - } "deferred_until_implemented" => { - "SDK outbox push reported Reticulum preview work as deferred until implemented without network delivery" + "SDK outbox push reported Reticulum work as deferred until implemented without network delivery" } - _ => "SDK outbox push reported Reticulum preview work without network delivery", + _ => "SDK outbox push reported Reticulum work without network delivery", } .to_owned()); } @@ -292,9 +250,6 @@ fn active_profile_view(config: &RuntimeConfig) -> TransportProfileView { None, None, None, - None, - None, - None, "configured", ), TransportProfileKind::Nostr => profile_view_from_parts( @@ -303,47 +258,26 @@ fn active_profile_view(config: &RuntimeConfig) -> TransportProfileView { None, None, None, - None, - None, - None, if config.transport.nostr_relay_urls.is_empty() { "unconfigured" } else { "configured" }, ), - TransportProfileKind::ReticulumPreview => profile_view_from_parts( - "reticulum_preview", + TransportProfileKind::Reticulum => profile_view_from_parts( + "reticulum", Vec::new(), - Some( - config - .transport - .reticulum_preview_behavior - .as_str() - .to_owned(), - ), - Some(config.transport.reticulum_preview_scope.clone()), - config.transport.reticulum_preview_agent_endpoint.clone(), - None, - None, - None, - "preview_unavailable", + Some(config.transport.reticulum_behavior.as_str().to_owned()), + Some(config.transport.reticulum_scope.clone()), + config.transport.reticulum_agent_endpoint.clone(), + "configured", ), - TransportProfileKind::Hybrid => profile_view_from_parts( - "hybrid", + TransportProfileKind::MultiTarget => profile_view_from_parts( + "multi_target", config.transport.nostr_relay_urls.clone(), - Some( - config - .transport - .reticulum_preview_behavior - .as_str() - .to_owned(), - ), - Some(config.transport.reticulum_preview_scope.clone()), - config.transport.reticulum_preview_agent_endpoint.clone(), - None, - None, - None, + Some(config.transport.reticulum_behavior.as_str().to_owned()), + Some(config.transport.reticulum_scope.clone()), + config.transport.reticulum_agent_endpoint.clone(), if config.transport.nostr_relay_urls.is_empty() { "unconfigured" } else { @@ -351,83 +285,34 @@ fn active_profile_view(config: &RuntimeConfig) -> TransportProfileView { }, ) .with_message( - "Hybrid transport publishes through configured Nostr relays and reports Reticulum preview status" + "Multi-target transport publishes through configured Nostr relays and reports Reticulum availability" .to_owned(), ), - TransportProfileKind::Proxy => { - let proxy_readiness = proxy_token_ready(config); - profile_view_from_parts( - "proxy", - Vec::new(), - None, - None, - None, - Some(config.transport.proxy.url.clone()), - config - .transport - .proxy - .token_file - .as_ref() - .map(|path| path.display().to_string()), - config.transport.proxy.token_secret_id.clone(), - if proxy_readiness.is_ok() { - "configured" - } else { - "unconfigured" - }, - ) - .with_message(proxy_readiness.err().map_or_else( - || "Proxy transport delegates delivery to the configured endpoint".to_owned(), - |error| error.to_string(), - )) - } } } fn profile_view_from_parts( profile_id: &str, nostr_relays: Vec<String>, - reticulum_preview_behavior: Option<String>, - reticulum_preview_scope: Option<String>, - reticulum_preview_agent_endpoint: Option<String>, - proxy_url: Option<String>, - proxy_token_file: Option<String>, - proxy_token_secret_id: Option<String>, + reticulum_behavior: Option<String>, + reticulum_scope: Option<String>, + reticulum_agent_endpoint: Option<String>, configured_state: &str, ) -> TransportProfileView { - let transport_statuses = transport_statuses_from_parts( - profile_id, - nostr_relays.as_slice(), - proxy_url.as_deref(), - configured_state, - ); + let transport_statuses = transport_statuses_from_parts(profile_id, nostr_relays.as_slice()); let profile_delivery_usable = transport_statuses .iter() .any(|status| status.usable_for_delivery); let message = match profile_id { "nostr" if profile_delivery_usable => "Nostr relay transport is configured for delivery", "nostr" => "Nostr transport requires configured Nostr relay targets", - "reticulum_preview" => RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, - "hybrid" if profile_delivery_usable => { - "Hybrid transport publishes through configured Nostr relays and reports Reticulum preview status" - } - "hybrid" => "Hybrid transport requires configured Nostr relay targets", - "proxy" if profile_delivery_usable => { - "Proxy transport delegates delivery to the configured endpoint" + "reticulum" => RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + "multi_target" if profile_delivery_usable => { + "Multi-target transport publishes through configured Nostr relays and reports Reticulum availability" } - "proxy" => "Proxy transport requires a configured token file or token secret id", + "multi_target" => "Multi-target transport requires configured Nostr relay targets", _ => "Local-only profile does not deliver to network transports", }; - let proxy_token_source = match ( - proxy_token_file.as_ref().filter(|value| !value.is_empty()), - proxy_token_secret_id - .as_ref() - .filter(|value| !value.is_empty()), - ) { - (Some(_), None) => Some("token_file".to_owned()), - (None, Some(_)) => Some("token_secret_id".to_owned()), - _ => None, - }; TransportProfileView { state: configured_state.to_owned(), source: TRANSPORT_SOURCE.to_owned(), @@ -437,13 +322,9 @@ fn profile_view_from_parts( profile_delivery_usable, message: message.to_owned(), nostr_relays, - reticulum_preview_behavior, - reticulum_preview_scope, - reticulum_preview_agent_endpoint, - proxy_url, - proxy_token_source, - proxy_token_file, - proxy_token_secret_id, + reticulum_behavior, + reticulum_scope, + reticulum_agent_endpoint, transport_statuses, actions: profile_actions(profile_id, profile_delivery_usable), } @@ -452,31 +333,24 @@ fn profile_view_from_parts( fn transport_statuses_from_parts( profile_id: &str, nostr_relays: &[String], - proxy_url: Option<&str>, - configured_state: &str, ) -> Vec<TransportRuntimeStatusView> { match profile_id { "nostr" => vec![transport_runtime_status_view(nostr_transport_status( profile_id, !nostr_relays.is_empty(), ))], - "reticulum_preview" => { - vec![transport_runtime_status_view( - reticulum_preview_transport_status(profile_id), - )] + "reticulum" => { + vec![transport_runtime_status_view(reticulum_transport_status( + profile_id, + ))] } - "hybrid" => vec![ + "multi_target" => vec![ transport_runtime_status_view(nostr_transport_status( profile_id, !nostr_relays.is_empty(), )), - transport_runtime_status_view(reticulum_preview_transport_status(profile_id)), + transport_runtime_status_view(reticulum_transport_status(profile_id)), ], - "proxy" => vec![transport_runtime_status_view(proxy_transport_status( - profile_id, - proxy_url, - configured_state == "configured", - ))], _ => vec![transport_runtime_status_view(local_transport_status( profile_id, ))], @@ -509,39 +383,18 @@ fn nostr_transport_status(profile_id: &str, targets_configured: bool) -> Radroot .with_profile_id(profile_id) } -fn reticulum_preview_transport_status(profile_id: &str) -> RadrootsTransportStatus { +fn reticulum_transport_status(profile_id: &str) -> RadrootsTransportStatus { RadrootsTransportStatus::new( RadrootsTransportKind::Reticulum, true, - RadrootsTransportImplementationState::PreviewUnavailable, + RadrootsTransportImplementationState::Real, false, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, ) .with_profile_id(profile_id) - .with_endpoint_uri(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI) -} - -fn proxy_transport_status( - profile_id: &str, - proxy_url: Option<&str>, - token_ready: bool, -) -> RadrootsTransportStatus { - let mut status = RadrootsTransportStatus::new( - RadrootsTransportKind::Proxy, - token_ready, - RadrootsTransportImplementationState::Real, - token_ready, - if token_ready { - "Proxy transport delegates delivery to the configured endpoint" - } else { - "Proxy transport requires a configured token file or token secret id" - }, - ) - .with_profile_id(profile_id); - if let Some(proxy_url) = proxy_url { - status = status.with_endpoint_uri(proxy_url); - } - status + .with_endpoint_uri(RADROOTS_RETICULUM_ENDPOINT_URI) + .with_maturity(RadrootsTransportCapabilityMaturity::Preview) + .with_availability(RadrootsTransportCapabilityAvailability::Unavailable) } fn transport_runtime_status_view(status: RadrootsTransportStatus) -> TransportRuntimeStatusView { @@ -551,6 +404,8 @@ fn transport_runtime_status_view(status: RadrootsTransportStatus) -> TransportRu endpoint_uri: status.endpoint_uri, configured: status.configured, implementation: transport_implementation_label(status.implementation).to_owned(), + maturity: transport_maturity_label(status.maturity).to_owned(), + availability: transport_availability_label(status.availability).to_owned(), usable_for_delivery: status.usable_for_delivery, capabilities: TransportOperationCapabilitiesView { deliver: status.capabilities.deliver, @@ -564,15 +419,31 @@ fn transport_implementation_label(state: RadrootsTransportImplementationState) - match state { RadrootsTransportImplementationState::Real => "real", RadrootsTransportImplementationState::Mock => "mock", - RadrootsTransportImplementationState::PreviewUnavailable => "preview_unavailable", } } -fn reticulum_preview_scope_for_profile(profile_id: &str, input: &OperationData) -> Option<String> { +fn transport_maturity_label(maturity: RadrootsTransportCapabilityMaturity) -> &'static str { + match maturity { + RadrootsTransportCapabilityMaturity::Preview => "preview", + RadrootsTransportCapabilityMaturity::Stable => "stable", + } +} + +fn transport_availability_label( + availability: RadrootsTransportCapabilityAvailability, +) -> &'static str { + match availability { + RadrootsTransportCapabilityAvailability::Available => "available", + RadrootsTransportCapabilityAvailability::Degraded => "degraded", + RadrootsTransportCapabilityAvailability::Unavailable => "unavailable", + } +} + +fn reticulum_scope_for_profile(profile_id: &str, input: &OperationData) -> Option<String> { match profile_id { - "reticulum_preview" | "hybrid" => Some( - string_input(input, "reticulum_preview_scope") - .unwrap_or(RADROOTS_RETICULUM_PREVIEW_SCOPE_ID) + "reticulum" | "multi_target" => Some( + string_input(input, "reticulum_scope") + .unwrap_or(RADROOTS_RETICULUM_SCOPE_ID) .to_owned(), ), _ => None, @@ -584,7 +455,7 @@ fn profile_actions(profile_id: &str, profile_delivery_usable: bool) -> Vec<Strin return Vec::new(); } match profile_id { - "reticulum_preview" => vec![ + "reticulum" => vec![ "radroots mesh status".to_owned(), "radroots transport profile get".to_owned(), ], @@ -594,61 +465,16 @@ fn profile_actions(profile_id: &str, profile_delivery_usable: bool) -> Vec<Strin .to_owned(), ] } - "hybrid" => { + "multi_target" => { vec![ - "radroots transport profile set --kind hybrid --nostr-relay wss://relay.example.com" + "radroots transport profile set --kind multi-target --nostr-relay wss://relay.example.com" .to_owned(), ] } - "proxy" => vec![ - "radroots transport profile set --kind proxy --proxy-url http://127.0.0.1:7070 --proxy-token-file <path>" - .to_owned(), - ], _ => vec!["radroots transport profile get".to_owned()], } } -fn validate_proxy_token_source( - token_file: Option<&str>, - token_secret_id: Option<&str>, -) -> Result<(), RuntimeError> { - match (token_file, token_secret_id) { - (None, None) => Err(RuntimeError::Config( - "transport profile `proxy` requires --proxy-token-file or --proxy-token-secret-id" - .to_owned(), - )), - (Some(file), None) if file.trim().is_empty() => Err(RuntimeError::Config( - "transport profile `proxy` requires a non-empty --proxy-token-file".to_owned(), - )), - (None, Some(secret_id)) if secret_id.trim().is_empty() => Err(RuntimeError::Config( - "transport profile `proxy` requires a non-empty --proxy-token-secret-id".to_owned(), - )), - (Some(_), Some(_)) => Err(RuntimeError::Config( - "transport profile `proxy` cannot set both --proxy-token-file and --proxy-token-secret-id" - .to_owned(), - )), - _ => Ok(()), - } -} - -pub fn proxy_token_ready(config: &RuntimeConfig) -> Result<(), RuntimeError> { - crate::runtime::sdk::validate_proxy_bearer_token(config) -} - -fn validate_proxy_token_material( - config: &RuntimeConfig, - url: &str, - token_file: Option<&str>, - token_secret_id: Option<&str>, -) -> Result<(), RuntimeError> { - let mut validation_config = config.clone(); - validation_config.transport.profile = TransportProfileKind::Proxy; - validation_config.transport.proxy.url = url.to_owned(); - validation_config.transport.proxy.token_file = token_file.map(PathBuf::from); - validation_config.transport.proxy.token_secret_id = token_secret_id.map(str::to_owned); - proxy_token_ready(&validation_config) -} - trait TransportProfileViewMessage { fn with_message(self, message: String) -> Self; fn summary(&self) -> TransportProfileSummaryView; @@ -725,24 +551,16 @@ mod tests { }; #[test] - fn transport_outbox_push_reports_reticulum_preview_without_attempts() { - let cases = [ - ( - PushOutboxEventState::PreviewUnavailable, - PushOutboxTargetOutcomeKind::PreviewUnavailable, - "preview_unavailable", - "SDK outbox push reported Reticulum preview work as preview unavailable without network delivery", - ), - ( - PushOutboxEventState::DeferredUntilImplemented, - PushOutboxTargetOutcomeKind::DeferredUntilImplemented, - "deferred_until_implemented", - "SDK outbox push reported Reticulum preview work as deferred until implemented without network delivery", - ), - ]; + fn transport_outbox_push_reports_reticulum_deferred_without_attempts() { + let cases = [( + PushOutboxEventState::DeferredUntilImplemented, + PushOutboxTargetOutcomeKind::DeferredUntilImplemented, + "deferred_until_implemented", + "SDK outbox push reported Reticulum work as deferred until implemented without network delivery", + )]; for (final_state, outcome_kind, expected_state, expected_reason) in cases { - let receipt = reticulum_preview_receipt(final_state, outcome_kind); + let receipt = reticulum_receipt(final_state, outcome_kind); assert_eq!(transport_outbox_push_state(&receipt, 0), expected_state); assert_eq!( @@ -752,7 +570,7 @@ mod tests { } } - fn reticulum_preview_receipt( + fn reticulum_receipt( final_state: PushOutboxEventState, outcome_kind: PushOutboxTargetOutcomeKind, ) -> PushOutboxReceipt { @@ -773,17 +591,14 @@ mod tests { quorum_met: false, targets: vec![PushOutboxTargetReceipt { transport_kind: "reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), - target_scope: Some("local_preview".to_owned()), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), + target_scope: Some(RADROOTS_RETICULUM_SCOPE_ID.to_owned()), target_label: None, outcome_kind, transport_outcome_kind: Some(match outcome_kind { PushOutboxTargetOutcomeKind::DeferredUntilImplemented => { PushOutboxTransportOutcomeKind::DeferredUntilImplemented } - PushOutboxTargetOutcomeKind::PreviewUnavailable => { - PushOutboxTransportOutcomeKind::TransportUnavailable - } _ => PushOutboxTransportOutcomeKind::TransportUnavailable, }), attempted: false, diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs @@ -4,8 +4,6 @@ use radroots_sdk::{ TradeValidationReceiptListReceipt, TradeValidationReceiptListRequest, TradeValidationReceiptNostrRelayOutcomeKind, TradeValidationReceiptNostrRelayOutcomeReceipt, TradeValidationReceiptTags, TradeValidationReceiptVerifyRequest, - TradeValidationReceiptWorkerEvidence, - TradeValidationReceiptWorkerEvidenceSelection as SdkWorkerEvidenceSelection, }; use radroots_trade::validation_receipt::{ RadrootsTradeCommitmentConfidence, RadrootsTradeValidationAuthority, @@ -240,11 +238,7 @@ pub fn list(config: &RuntimeConfig, args: &ValidationReceiptListArgs) -> Validat "validation receipt list requires non-empty `order_id`", ); } - let request = match TradeValidationReceiptListRequest::parse(order_id).and_then(|request| { - request.try_with_trusted_worker_pubkeys( - config.rhi.trusted_worker_pubkeys.iter().map(String::as_str), - ) - }) { + let request = match TradeValidationReceiptListRequest::parse(order_id) { Ok(request) => request, Err(error) => { return list_sdk_error_view(order_id, CliSdkAdapterError::Sdk(error)); @@ -274,16 +268,13 @@ fn inspect_event( "validation receipt command requires non-empty `receipt_event_id`", ); } - let trusted_worker_pubkeys = config.rhi.trusted_worker_pubkeys.iter().map(String::as_str); let session = match CliSdkSession::connect(config) { Ok(session) => session, Err(error) => return inspection_sdk_error_view(receipt_event_id, error), }; match intent { ValidationReceiptCommandIntent::Inspect => { - let request = match TradeValidationReceiptInspectRequest::parse(receipt_event_id) - .and_then(|request| request.try_with_trusted_worker_pubkeys(trusted_worker_pubkeys)) - { + let request = match TradeValidationReceiptInspectRequest::parse(receipt_event_id) { Ok(request) => request, Err(error) => { return inspection_sdk_error_view( @@ -308,9 +299,7 @@ fn inspect_event( } } ValidationReceiptCommandIntent::Verify => { - let request = match TradeValidationReceiptVerifyRequest::parse(receipt_event_id) - .and_then(|request| request.try_with_trusted_worker_pubkeys(trusted_worker_pubkeys)) - { + let request = match TradeValidationReceiptVerifyRequest::parse(receipt_event_id) { Ok(request) => request, Err(error) => { return inspection_sdk_error_view( @@ -398,7 +387,7 @@ fn inspected_event_view( let order_id = sdk_receipt.tags.order_id.clone(); let proof_verification = proof_verification_view_for_receipt( &sdk_receipt.receipt, - sdk_worker_evidence_selection(sdk_receipt.worker_evidence), + ValidationReceiptWorkerEvidenceSelection::default(), ); let accepted = match intent { ValidationReceiptCommandIntent::Inspect => { @@ -492,7 +481,7 @@ fn list_from_sdk_receipt( for sdk_event in sdk_receipt.receipts { let proof_verification = proof_verification_view_for_receipt( &sdk_event.receipt, - sdk_worker_evidence_selection(sdk_event.worker_evidence), + ValidationReceiptWorkerEvidenceSelection::default(), ); if proof_state_is_invalid(proof_verification.state.as_str()) { invalid_receipts.push(ValidationReceiptInvalidCandidateView { @@ -639,8 +628,7 @@ fn sdk_error_parts(error: RadrootsSdkError) -> ValidationReceiptSdkErrorParts { radroots_sdk::RadrootsSdkErrorClass::Transport | radroots_sdk::RadrootsSdkErrorClass::Storage | radroots_sdk::RadrootsSdkErrorClass::Clock - | radroots_sdk::RadrootsSdkErrorClass::Authorization - | radroots_sdk::RadrootsSdkErrorClass::LocalMutation => "network_unavailable", + | radroots_sdk::RadrootsSdkErrorClass::Authorization => "network_unavailable", _ => "network_unavailable", }, }; @@ -1197,39 +1185,6 @@ fn invalid_candidate_view( } } -fn sdk_worker_evidence_selection( - selection: SdkWorkerEvidenceSelection, -) -> ValidationReceiptWorkerEvidenceSelection { - ValidationReceiptWorkerEvidenceSelection { - trusted: selection.trusted.map(worker_evidence_view), - untrusted: selection.untrusted.map(worker_evidence_view), - } -} - -fn worker_evidence_view( - evidence: TradeValidationReceiptWorkerEvidence, -) -> ValidationReceiptWorkerEvidenceView { - ValidationReceiptWorkerEvidenceView { - result_event_id: evidence.result_event_id.as_str().to_owned(), - author: evidence.author.as_str().to_owned(), - validation_authority: evidence - .validation_authority - .map(|authority| authority.as_str().to_owned()), - commitment_confidence: evidence - .commitment_confidence - .map(|confidence| confidence.as_str().to_owned()), - status: evidence.status, - prover_backend: evidence.prover_backend, - proof_mode: evidence.proof_mode, - proof_system: evidence.proof_system, - proof_generated: evidence.proof_generated, - sp1_execute_checked: evidence.sp1_execute_checked, - sp1_execute_public_values_hash: evidence.sp1_execute_public_values_hash, - cryptographic_proof_verified: evidence.cryptographic_proof_verified, - public_values_hash: evidence.public_values_hash, - } -} - fn attempted_transport_endpoints( relays: &[TradeValidationReceiptNostrRelayOutcomeReceipt], ) -> Vec<String> { diff --git a/src/view/runtime.rs b/src/view/runtime.rs @@ -634,7 +634,6 @@ pub struct SdkOutboxStatusView { pub retryable_events: i64, pub terminal_events: i64, pub failed_terminal_events: i64, - pub preview_unavailable_events: i64, pub deferred_until_implemented_events: i64, pub ready_signed_events: i64, pub publishing_events: i64, @@ -3155,8 +3154,6 @@ pub struct SyncQueueView { #[serde(skip_serializing_if = "Option::is_none")] pub failed_terminal_count: Option<usize>, #[serde(skip_serializing_if = "Option::is_none")] - pub preview_unavailable_count: Option<usize>, - #[serde(skip_serializing_if = "Option::is_none")] pub deferred_until_implemented_count: Option<usize>, #[serde(skip_serializing_if = "Option::is_none")] pub ready_signed_count: Option<usize>, @@ -3273,19 +3270,11 @@ pub struct TransportProfileView { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub nostr_relays: Vec<String>, #[serde(skip_serializing_if = "Option::is_none")] - pub reticulum_preview_behavior: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub reticulum_preview_scope: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub reticulum_preview_agent_endpoint: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub proxy_url: Option<String>, + pub reticulum_behavior: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] - pub proxy_token_source: Option<String>, + pub reticulum_scope: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] - pub proxy_token_file: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub proxy_token_secret_id: Option<String>, + pub reticulum_agent_endpoint: Option<String>, pub transport_statuses: Vec<TransportRuntimeStatusView>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub actions: Vec<String>, @@ -3317,6 +3306,8 @@ pub struct TransportRuntimeStatusView { pub endpoint_uri: Option<String>, pub configured: bool, pub implementation: String, + pub maturity: String, + pub availability: String, pub usable_for_delivery: bool, pub capabilities: TransportOperationCapabilitiesView, pub message: String, @@ -3337,7 +3328,6 @@ pub struct TransportOutboxStatusView { pub pending_count: i64, pub retryable_count: i64, pub terminal_count: i64, - pub preview_unavailable_count: i64, pub deferred_until_implemented_count: i64, pub ready_signed_count: i64, pub publishing_count: i64, @@ -3423,9 +3413,9 @@ pub struct RpcStatusView { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub supported_signer_modes: Vec<String>, #[serde(skip_serializing_if = "Option::is_none")] - pub daemon_proxy_enabled: Option<bool>, + pub daemon_execution_enabled: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] - pub daemon_proxy_ready: Option<bool>, + pub daemon_execution_ready: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] pub relay_count: Option<usize>, #[serde(skip_serializing_if = "Option::is_none")] @@ -3575,6 +3565,8 @@ pub struct SyncTransportStatusView { pub endpoint_uri: Option<String>, pub configured: bool, pub implementation: String, + pub maturity: String, + pub availability: String, pub usable_for_delivery: bool, pub capabilities: TransportOperationCapabilitiesView, pub message: String, diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs @@ -926,6 +926,8 @@ fn local_listing_publish_fails_without_local_account_authority() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000004", "listing", "publish", listing_file.to_string_lossy().as_ref(), @@ -1280,6 +1282,8 @@ fn local_farm_publish_fails_without_configured_relay() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000003", "farm", "publish", ]); @@ -1420,6 +1424,7 @@ fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() { "pickup", ]); let relay_url = "ws://127.0.0.1:9"; + let idempotency_key = "01890f3c-7f2a-7b01-8000-000000000001"; sandbox.write_nostr_transport_profile(&[relay_url]); let (output, value) = sandbox.json_output(&[ @@ -1428,7 +1433,7 @@ fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() { "--approval-token", "approve", "--idempotency-key", - "farm_partial", + idempotency_key, "farm", "publish", ]); @@ -1455,8 +1460,14 @@ fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() { .len(), 64 ); - assert_eq!(detail["profile"]["idempotency_key"], "farm_partial:profile"); - assert_eq!(detail["farm"]["idempotency_key"], "farm_partial:farm"); + assert_eq!( + detail["profile"]["idempotency_key"], + "01890f3c-7f2a-7b01-8000-9db91832364f" + ); + assert_eq!( + detail["farm"]["idempotency_key"], + "01890f3c-7f2a-7b01-8000-f953bff1d106" + ); assert_eq!(detail["actions"][0], "radroots sync push"); assert_eq!(detail["farm"]["target_transport_endpoints"][0], relay_url); assert_relay_url( @@ -1508,6 +1519,7 @@ fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() { "pickup", ]); let relay_url = "ws://127.0.0.1:9"; + let idempotency_key = "01890f3c-7f2a-7b01-8000-000000000002"; sandbox.write_nostr_transport_profile(&[relay_url]); let (output, value) = sandbox.json_output(&[ @@ -1516,7 +1528,7 @@ fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() { "--approval-token", "approve", "--idempotency-key", - "farm_success", + idempotency_key, "farm", "publish", ]); @@ -1538,8 +1550,14 @@ fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() { .len(), 64 ); - assert_eq!(detail["profile"]["idempotency_key"], "farm_success:profile"); - assert_eq!(detail["farm"]["idempotency_key"], "farm_success:farm"); + assert_eq!( + detail["profile"]["idempotency_key"], + "01890f3c-7f2a-7b01-8000-c7f426f40246" + ); + assert_eq!( + detail["farm"]["idempotency_key"], + "01890f3c-7f2a-7b01-8000-692916b32301" + ); assert_no_removed_command_reference(&value, &["farm", "publish"]); assert_no_daemon_runtime_reference(&value, &["farm", "publish"]); @@ -2153,6 +2171,8 @@ fn local_seller_publish_commands_attempt_configured_relay() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000003", "farm", "publish", ]); @@ -2189,6 +2209,8 @@ fn local_seller_publish_commands_attempt_configured_relay() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000004", "listing", "publish", listing_file_arg.as_ref(), @@ -2228,6 +2250,8 @@ fn local_seller_publish_commands_attempt_configured_relay() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000005", "listing", "archive", listing_file_arg.as_ref(), diff --git a/tests/support/mod.rs b/tests/support/mod.rs @@ -15,7 +15,7 @@ use radroots_runtime_store::{ RuntimeStoreRecordInput, RuntimeStoreRecordStatus, SourceRuntime, }; use radroots_secret_vault::RadrootsSecretVault; -use radroots_sql_core::{SqlExecutor, SqliteExecutor}; +use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor}; use serde_json::{Value, json}; use tempfile::TempDir; @@ -140,7 +140,7 @@ impl RadrootsCliSandbox { if !path.exists() { return Vec::new(); } - let executor = SqliteExecutor::open(path).expect("open runtime store db"); + let executor = SqlxSqliteExecutor::open(path).expect("open runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate runtime store db"); store @@ -324,7 +324,7 @@ pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) sig: "f".repeat(128), }) .expect("seed listing event"); - let executor = SqliteExecutor::open(Path::new(db_path)).expect("open replica db"); + let executor = SqlxSqliteExecutor::open(Path::new(db_path)).expect("open replica db"); assert_eq!( radroots_replica_ingest_event(&executor, &event).expect("ingest listing"), RadrootsReplicaIngestOutcome::Applied @@ -335,7 +335,7 @@ pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) #[allow(dead_code)] pub fn seed_market_refresh_provenance(sandbox: &RadrootsCliSandbox, relay_urls: &[&str]) { - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); executor .exec( "CREATE TABLE IF NOT EXISTS radroots_cli_sync_run ( @@ -433,7 +433,7 @@ fn seed_orderable_listing_signed_event( let database_path = sandbox.runtime_store_db_path(); fs::create_dir_all(database_path.parent().expect("runtime store parent")) .expect("runtime store parent"); - let executor = SqliteExecutor::open(database_path).expect("open runtime store"); + let executor = SqlxSqliteExecutor::open(database_path).expect("open runtime store"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate runtime store"); store @@ -467,7 +467,7 @@ fn seed_orderable_listing_signed_event( } pub fn remove_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) { - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); let params = serde_json::to_string(&vec![listing_addr]).expect("delete listing params"); executor .exec( @@ -482,7 +482,7 @@ pub fn update_orderable_listing_available_amount( listing_addr: &str, available_amount: i64, ) { - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); let params = serde_json::to_string(&serde_json::json!([available_amount, listing_addr])) .expect("update listing params"); executor @@ -498,7 +498,7 @@ pub fn update_orderable_listing_primary_bin_id( listing_addr: &str, primary_bin_id: Option<&str>, ) { - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); let params = serde_json::to_string(&serde_json::json!([primary_bin_id, listing_addr])) .expect("update listing primary bin params"); executor @@ -510,7 +510,7 @@ pub fn update_orderable_listing_primary_bin_id( } pub fn duplicate_orderable_listing_row(sandbox: &RadrootsCliSandbox, listing_addr: &str) { - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); let params = serde_json::to_string(&json!([ "33333333-3333-3333-3333-333333333333", listing_addr @@ -531,7 +531,7 @@ pub fn replace_latest_listing_event_id( ) { let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr); let key = format!("{KIND_LISTING}:{seller_pubkey}:{listing_id}"); - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica db"); let params = serde_json::to_string(&vec![event_id, key.as_str()]).expect("update params"); executor .exec( diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -1,7 +1,6 @@ mod support; use std::fs; -use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::path::{Path, PathBuf}; use std::sync::{ @@ -31,7 +30,7 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, }; use radroots_outbox::{ - RadrootsOutbox, RadrootsOutboxDeliveryPlanInput, RadrootsOutboxReticulumPreviewBehavior, + RadrootsOutbox, RadrootsOutboxDeliveryPlanInput, RadrootsOutboxReticulumBehavior, RadrootsOutboxSignedOperationInput, }; use radroots_replica_schema::farm::IFarmFields; @@ -41,10 +40,10 @@ use radroots_runtime_store::{ BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, PublishOutboxStatus, RuntimeStore, RuntimeStoreRecordFamily, RuntimeStoreRecordInput, RuntimeStoreRecordStatus, SourceRuntime, }; -use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::SqlxSqliteExecutor; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, - RadrootsTransportKind, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportKind, + RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, }; use serde_json::Value; use serde_json::json; @@ -187,12 +186,6 @@ fn test_pubkey(value: &str) -> RadrootsPublicKey { value.parse().expect("valid public key") } -fn proxy_token_file(sandbox: &RadrootsCliSandbox) -> PathBuf { - let path = sandbox.root().join("proxy.token"); - fs::write(&path, "proxy_test_token\n").expect("write proxy token file"); - path -} - fn configure_nostr_transport(sandbox: &RadrootsCliSandbox, relay_url: &str) { sandbox.write_nostr_transport_profile(&[relay_url]); } @@ -218,16 +211,10 @@ fn assert_transport_capabilities(status: &Value, deliver: bool, fetch: bool) { assert_eq!(status["capabilities"]["fetch"], fetch); } -fn write_proxy_transport_config(sandbox: &RadrootsCliSandbox, body: &str) -> PathBuf { - sandbox.write_app_config( - format!("[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\n{body}").as_str(), - ) -} - fn sdk_outbox_path(sandbox: &RadrootsCliSandbox) -> PathBuf { sandbox .root() - .join("data/apps/cli/replica/sdk/outbox.sqlite") + .join("data/apps/cli/replica/sdk/runtime.sqlite") } fn post_outbox_draft(public_key_hex: &str, content: &str) -> RadrootsEventDraft { @@ -257,7 +244,7 @@ fn seed_signed_transport_outbox_event( content: &str, transport_profile_id: &str, target: RadrootsTransportTarget, - behavior: RadrootsOutboxReticulumPreviewBehavior, + behavior: RadrootsOutboxReticulumBehavior, claim_for_publish: bool, ) { let outbox_path = sdk_outbox_path(sandbox); @@ -285,7 +272,7 @@ fn seed_signed_transport_outbox_event( RadrootsTransportSatisfactionPolicy::all_accepted(), vec![target], ) - .with_reticulum_preview_behavior(behavior), + .with_reticulum_behavior(behavior), true, 1_700_000_000_007, 1_700_000_000_000, @@ -348,52 +335,6 @@ impl RelayFetchServer { } } -struct ProxyJsonRpcServer { - endpoint: String, - handle: JoinHandle<Value>, -} - -impl ProxyJsonRpcServer { - fn once(expected_token: Option<&'static str>) -> Self { - let listener = TcpListener::bind("127.0.0.1:0").expect("bind proxy"); - listener.set_nonblocking(true).expect("proxy nonblocking"); - let endpoint = format!("http://{}", listener.local_addr().expect("proxy addr")); - let handle = thread::spawn(move || { - let deadline = Instant::now() + Duration::from_secs(30); - loop { - match listener.accept() { - Ok((stream, _)) => { - return handle_proxy_connection(stream, expected_token); - } - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - assert!( - Instant::now() < deadline, - "timed out waiting for proxy request" - ); - thread::sleep(Duration::from_millis(10)); - } - Err(error) => panic!("accept proxy connection: {error}"), - } - } - }); - Self { endpoint, handle } - } - - fn endpoint(&self) -> &str { - self.endpoint.as_str() - } - - fn join(self) -> Value { - self.handle.join().expect("proxy server join") - } -} - -#[derive(Clone, Copy)] -enum Nip46RelayFinish { - SignResponse, - ProductPublish, -} - struct Nip46RelayReport { connection_count: usize, req_count: usize, @@ -408,7 +349,6 @@ struct Nip46RelayState { published_events: Mutex<Vec<RadrootsNostrEvent>>, pending_responses: Mutex<Vec<RadrootsNostrEvent>>, done: AtomicBool, - finish: Nip46RelayFinish, } struct Nip46RelayServer { @@ -418,7 +358,7 @@ struct Nip46RelayServer { } impl Nip46RelayServer { - fn new(remote_keys: Keys, user_keys: Keys, finish: Nip46RelayFinish) -> Self { + fn new(remote_keys: Keys, user_keys: Keys) -> Self { let listener = TcpListener::bind("127.0.0.1:0").expect("bind nip46 relay"); listener.set_nonblocking(true).expect("nip46 nonblocking"); let endpoint = format!("ws://{}", listener.local_addr().expect("nip46 addr")); @@ -429,7 +369,6 @@ impl Nip46RelayServer { published_events: Mutex::new(Vec::new()), pending_responses: Mutex::new(Vec::new()), done: AtomicBool::new(false), - finish, }); let thread_state = Arc::clone(&state); let remote_keys = Arc::new(remote_keys); @@ -614,9 +553,6 @@ fn handle_nip46_sign_request( .sign_request_count .lock() .expect("sign request count lock") += 1; - if matches!(state.finish, Nip46RelayFinish::SignResponse) { - state.done.store(true, Ordering::SeqCst); - } } fn send_pending_nip46_responses( @@ -659,9 +595,7 @@ fn handle_nip46_product_publish( .lock() .expect("published events lock") .push(event); - if matches!(state.finish, Nip46RelayFinish::ProductPublish) { - state.done.store(true, Ordering::SeqCst); - } + state.done.store(true, Ordering::SeqCst); } fn nip46_response_event( @@ -727,112 +661,6 @@ signer_session_ref = "{}" ) } -fn handle_proxy_connection(mut stream: TcpStream, expected_token: Option<&str>) -> Value { - let mut bytes = Vec::new(); - let mut buffer = [0_u8; 1024]; - let body_start = loop { - let read = stream.read(&mut buffer).expect("read proxy"); - assert!(read > 0, "proxy request closed before headers"); - bytes.extend_from_slice(&buffer[..read]); - if let Some(index) = http_body_start(&bytes) { - break index; - } - }; - let headers = String::from_utf8(bytes[..body_start].to_vec()).expect("headers utf8"); - let content_length = http_content_length(headers.as_str()); - while bytes.len() < body_start + content_length { - let read = stream.read(&mut buffer).expect("read proxy body"); - assert!(read > 0, "proxy request closed before body"); - bytes.extend_from_slice(&buffer[..read]); - } - let body = String::from_utf8(bytes[body_start..body_start + content_length].to_vec()) - .expect("body utf8"); - if let Some(expected_token) = expected_token { - assert!( - headers - .to_ascii_lowercase() - .contains(format!("authorization: bearer {expected_token}").as_str()), - "proxy request missing expected bearer auth: {headers}" - ); - } - let request: Value = serde_json::from_str(body.as_str()).expect("proxy json"); - assert_eq!(request["jsonrpc"], "2.0"); - assert_eq!(request["method"], "transport.publish.event"); - assert!(request["params"].get("event").is_none()); - let event = proxy_request_raw_event(&request); - let target_result = json!({ - "transport_kind": "nostr", - "endpoint_uri": "wss://daemon-resolved.radroots.test", - "source": "daemon_default", - "attempted": true, - "outcome_kind": "accepted" - }); - let response = json!({ - "jsonrpc": "2.0", - "id": request["id"], - "result": { - "deduplicated": false, - "job": { - "job_id": "cli-proxy-job-1", - "status": "delivery_satisfied", - "terminal": true, - "delivery_satisfied": true, - "event_id": event["id"].clone(), - "pubkey": event["pubkey"].clone(), - "event_kind": event["kind"].clone(), - "target_policy": request["params"]["target_policy"], - "delivery_policy": request["params"]["delivery_policy"], - "target_count": 1, - "acknowledged_count": 1, - "retryable_count": 0, - "terminal_count": 0, - "requested_at_ms": 1_700_000_000_000_i64, - "completed_at_ms": 1_700_000_000_001_i64, - "targets": [target_result] - } - } - }); - let response_body = response.to_string(); - let raw_response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{response_body}", - response_body.len() - ); - stream - .write_all(raw_response.as_bytes()) - .expect("write proxy response"); - request -} - -fn proxy_request_raw_event(request: &Value) -> Value { - serde_json::from_str( - request["params"]["raw_event_json"] - .as_str() - .expect("proxy raw event json"), - ) - .expect("proxy raw event") -} - -fn http_body_start(bytes: &[u8]) -> Option<usize> { - bytes - .windows(4) - .position(|window| window == b"\r\n\r\n") - .map(|index| index + 4) -} - -fn http_content_length(headers: &str) -> usize { - headers - .lines() - .find_map(|line| { - let (name, value) = line.split_once(':')?; - if name.eq_ignore_ascii_case("content-length") { - Some(value.trim().parse::<usize>().expect("content length")) - } else { - None - } - }) - .expect("content-length header") -} - fn handle_relay_fetch_connection(stream: TcpStream, events: Vec<RadrootsNostrEvent>) { let mut websocket = tungstenite::accept(stream).expect("accept fetch websocket"); let subscription_id = read_relay_req_subscription_id(&mut websocket); @@ -869,7 +697,7 @@ fn read_relay_req_subscription_id(websocket: &mut tungstenite::WebSocket<TcpStre } fn seed_derived_projection_sync_farm(sandbox: &RadrootsCliSandbox, d_tag: &str, pubkey: &str) { - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica"); migrations::run_all_up(&executor).expect("replica migrations"); farm::create( &executor, @@ -1151,7 +979,7 @@ fn append_app_local_record(input: RuntimeStoreRecordInput, sandbox: &RadrootsCli let database_path = sandbox.runtime_store_db_path(); fs::create_dir_all(database_path.parent().expect("runtime store parent")) .expect("runtime store parent"); - let executor = SqliteExecutor::open(database_path).expect("open runtime store"); + let executor = SqlxSqliteExecutor::open(database_path).expect("open runtime store"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate runtime store"); store @@ -1534,16 +1362,16 @@ fn root_help_explains_transport_and_mesh_surfaces() { let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); assert!(stdout.contains("Manage transport profiles and outbox delivery.")); - assert!(stdout.contains("Inspect mesh scope and Reticulum preview policy.")); + assert!(stdout.contains("Inspect mesh scope and Reticulum admission policy.")); assert!(stdout.contains("Inspect local readiness and mode-specific recovery steps")); assert!(stdout.contains("Show effective configuration and publish-plane readiness.")); } #[test] -fn transport_profile_reticulum_preview_output_is_transport_specific() { +fn transport_profile_reticulum_output_is_transport_specific() { let sandbox = RadrootsCliSandbox::new(); sandbox.write_app_config( - "[transport]\nprofile = \"reticulum_preview\"\n\n[transport.reticulum_preview]\nbehavior = \"defer_delivery_plans\"\n", + "[transport]\nprofile = \"reticulum\"\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n", ); let value = sandbox.json_success(&["--format", "json", "transport", "profile", "get"]); @@ -1551,15 +1379,17 @@ fn transport_profile_reticulum_preview_output_is_transport_specific() { let actions = result["actions"].as_array().expect("actions"); assert_eq!(value["operation_id"], "transport.profile.get"); - assert_eq!(result["profile_id"], "reticulum_preview"); - assert_eq!(result["profile_kind"], "reticulum_preview"); + assert_eq!(result["profile_id"], "reticulum"); + assert_eq!(result["profile_kind"], "reticulum"); assert_eq!(result["profile_delivery_usable"], false); assert!(result.get("transport_kind").is_none()); assert!(result.get("implementation_state").is_none()); assert_eq!(result["transport_statuses"][0]["transport"], "reticulum"); + assert_eq!(result["transport_statuses"][0]["implementation"], "real"); + assert_eq!(result["transport_statuses"][0]["maturity"], "preview"); assert_eq!( - result["transport_statuses"][0]["implementation"], - "preview_unavailable" + result["transport_statuses"][0]["availability"], + "unavailable" ); assert_eq!(result["transport_statuses"][0]["configured"], true); assert_eq!( @@ -1572,8 +1402,8 @@ fn transport_profile_reticulum_preview_output_is_transport_specific() { RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE ); assert_no_removed_transport_status_fields(&result["transport_statuses"][0]); - assert_eq!(result["reticulum_preview_behavior"], "defer_delivery_plans"); - assert_eq!(result["reticulum_preview_scope"], "local_preview"); + assert_eq!(result["reticulum_behavior"], "defer_delivery_plans"); + assert_eq!(result["reticulum_scope"], "local"); assert_eq!(result["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE); assert!( !result["message"] @@ -1623,7 +1453,7 @@ fn transport_profile_local_only_output_uses_profile_and_local_status_fields() { } #[test] -fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() { +fn transport_profile_set_multi_target_persists_nostr_and_reticulum_config() { let sandbox = RadrootsCliSandbox::new(); let value = sandbox.json_success(&[ @@ -1633,20 +1463,20 @@ fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() { "profile", "set", "--kind", - "hybrid", + "multi-target", "--nostr-relay", "wss://relay.example.com", - "--reticulum-preview-behavior", + "--reticulum-behavior", "defer-delivery-plans", - "--reticulum-preview-scope", + "--reticulum-scope", "farmers_market", - "--reticulum-preview-agent-endpoint", + "--reticulum-agent-endpoint", "reticulum-agent:local", ]); assert_eq!(value["operation_id"], "transport.profile.set"); - assert_eq!(value["result"]["profile_id"], "hybrid"); - assert_eq!(value["result"]["profile_kind"], "hybrid"); + assert_eq!(value["result"]["profile_id"], "multi_target"); + assert_eq!(value["result"]["profile_kind"], "multi_target"); assert!(value["result"].get("transport_kind").is_none()); assert_eq!(value["result"]["state"], "configured"); assert_eq!( @@ -1680,7 +1510,15 @@ fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() { ); assert_eq!( value["result"]["transport_statuses"][1]["implementation"], - "preview_unavailable" + "real" + ); + assert_eq!( + value["result"]["transport_statuses"][1]["maturity"], + "preview" + ); + assert_eq!( + value["result"]["transport_statuses"][1]["availability"], + "unavailable" ); assert_eq!(value["result"]["transport_statuses"][1]["configured"], true); assert_eq!( @@ -1691,46 +1529,46 @@ fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() { assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]); assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][1]); assert_eq!( - value["result"]["reticulum_preview_behavior"], + value["result"]["reticulum_behavior"], "defer_delivery_plans" ); - assert_eq!(value["result"]["reticulum_preview_scope"], "farmers_market"); + assert_eq!(value["result"]["reticulum_scope"], "farmers_market"); assert_eq!( - value["result"]["reticulum_preview_agent_endpoint"], + value["result"]["reticulum_agent_endpoint"], "reticulum-agent:local" ); let value = sandbox.json_success(&["--format", "json", "transport", "profile", "get"]); - assert_eq!(value["result"]["profile_id"], "hybrid"); + assert_eq!(value["result"]["profile_id"], "multi_target"); assert_eq!( value["result"]["nostr_relays"][0], "wss://relay.example.com" ); assert_eq!( - value["result"]["reticulum_preview_behavior"], + value["result"]["reticulum_behavior"], "defer_delivery_plans" ); - assert_eq!(value["result"]["reticulum_preview_scope"], "farmers_market"); + assert_eq!(value["result"]["reticulum_scope"], "farmers_market"); assert_eq!( - value["result"]["reticulum_preview_agent_endpoint"], + value["result"]["reticulum_agent_endpoint"], "reticulum-agent:local" ); let config = fs::read_to_string(sandbox.root().join("config/apps/cli/config.toml")) .expect("read app config"); - assert!(config.contains("profile = \"hybrid\"")); + assert!(config.contains("profile = \"multi_target\"")); assert!(config.contains("[transport.nostr]")); - assert!(config.contains("[transport.reticulum_preview]")); + assert!(config.contains("[transport.reticulum]")); assert!(config.contains("behavior = \"defer_delivery_plans\"")); assert!(config.contains("scope = \"farmers_market\"")); assert!(config.contains("agent_endpoint = \"reticulum-agent:local\"")); } #[test] -fn transport_status_hybrid_output_reports_nostr_and_reticulum_rows() { +fn transport_status_multi_target_output_reports_nostr_and_reticulum_rows() { let sandbox = RadrootsCliSandbox::new(); sandbox.write_app_config( - "[transport]\nprofile = \"hybrid\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.example.com\"]\n\n[transport.reticulum_preview]\nbehavior = \"defer_delivery_plans\"\n", + "[transport]\nprofile = \"multi_target\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.example.com\"]\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n", ); let value = sandbox.json_success(&["--format", "json", "transport", "status"]); @@ -1740,8 +1578,8 @@ fn transport_status_hybrid_output_reports_nostr_and_reticulum_rows() { let reticulum = &transports[1]; assert_eq!(value["operation_id"], "transport.status"); - assert_eq!(result["active_profile"]["profile_id"], "hybrid"); - assert_eq!(result["active_profile"]["profile_kind"], "hybrid"); + assert_eq!(result["active_profile"]["profile_id"], "multi_target"); + assert_eq!(result["active_profile"]["profile_kind"], "multi_target"); assert_eq!(result["active_profile"]["profile_delivery_usable"], true); assert_eq!(transports.len(), 2); assert_eq!(nostr["transport"], "nostr"); @@ -1750,7 +1588,9 @@ fn transport_status_hybrid_output_reports_nostr_and_reticulum_rows() { assert_eq!(nostr["usable_for_delivery"], true); assert_transport_capabilities(nostr, true, false); assert_eq!(reticulum["transport"], "reticulum"); - assert_eq!(reticulum["implementation"], "preview_unavailable"); + assert_eq!(reticulum["implementation"], "real"); + assert_eq!(reticulum["maturity"], "preview"); + assert_eq!(reticulum["availability"], "unavailable"); assert_eq!(reticulum["configured"], true); assert_eq!(reticulum["usable_for_delivery"], false); assert_transport_capabilities(reticulum, false, false); @@ -1784,10 +1624,10 @@ fn transport_status_nostr_output_uses_canonical_transport_row() { } #[test] -fn transport_status_reticulum_preview_output_reports_unusable_preview_state() { +fn transport_status_reticulum_output_reports_unusable_unavailable_state() { let sandbox = RadrootsCliSandbox::new(); sandbox.write_app_config( - "[transport]\nprofile = \"reticulum_preview\"\n\n[transport.reticulum_preview]\nbehavior = \"defer_delivery_plans\"\n", + "[transport]\nprofile = \"reticulum\"\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n", ); let value = sandbox.json_success(&["--format", "json", "transport", "status"]); @@ -1798,14 +1638,13 @@ fn transport_status_reticulum_preview_output_reports_unusable_preview_state() { assert_eq!(value["operation_id"], "transport.status"); assert_eq!(result["state"], "ready"); assert_eq!(transports.len(), 1); - assert_eq!(result["active_profile"]["profile_id"], "reticulum_preview"); - assert_eq!( - result["active_profile"]["profile_kind"], - "reticulum_preview" - ); + assert_eq!(result["active_profile"]["profile_id"], "reticulum"); + assert_eq!(result["active_profile"]["profile_kind"], "reticulum"); assert_eq!(result["active_profile"]["profile_delivery_usable"], false); assert_eq!(active["transport"], "reticulum"); - assert_eq!(active["implementation"], "preview_unavailable"); + assert_eq!(active["implementation"], "real"); + assert_eq!(active["maturity"], "preview"); + assert_eq!(active["availability"], "unavailable"); assert_eq!(active["configured"], true); assert_eq!(active["usable_for_delivery"], false); assert_transport_capabilities(active, false, false); @@ -1825,28 +1664,28 @@ fn transport_status_reticulum_preview_output_reports_unusable_preview_state() { } #[test] -fn transport_outbox_status_human_output_reports_preview_deferred_and_publishing_counts() { +fn transport_outbox_status_human_output_reports_reticulum_deferred_and_publishing_counts() { let sandbox = RadrootsCliSandbox::new(); seed_signed_transport_outbox_event( &sandbox, - "preview unavailable", - "reticulum_preview", + "deferred until implemented", + "reticulum", transport_target( RadrootsTransportKind::Reticulum, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, + RADROOTS_RETICULUM_ENDPOINT_URI, ), - RadrootsOutboxReticulumPreviewBehavior::RejectDeliveryAttempts, + RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts, false, ); seed_signed_transport_outbox_event( &sandbox, "deferred until implemented", - "reticulum_preview", + "reticulum", transport_target( RadrootsTransportKind::Reticulum, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, + RADROOTS_RETICULUM_ENDPOINT_URI, ), - RadrootsOutboxReticulumPreviewBehavior::DeferDeliveryPlans, + RadrootsOutboxReticulumBehavior::DeferDeliveryPlans, false, ); seed_signed_transport_outbox_event( @@ -1854,7 +1693,7 @@ fn transport_outbox_status_human_output_reports_preview_deferred_and_publishing_ "publishing", "nostr", transport_target(RadrootsTransportKind::Nostr, ORDERABLE_LISTING_RELAY), - RadrootsOutboxReticulumPreviewBehavior::RejectDeliveryAttempts, + RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts, true, ); @@ -1872,8 +1711,7 @@ fn transport_outbox_status_human_output_reports_preview_deferred_and_publishing_ let stdout = String::from_utf8(output.stdout).expect("terminal stdout"); assert!(stdout.contains("Transport Outbox"), "{stdout}"); - assert_terminal_field(&stdout, "Preview unavailable", "1"); - assert_terminal_field(&stdout, "Deferred", "1"); + assert_terminal_field(&stdout, "Deferred", "2"); assert_terminal_field(&stdout, "Publishing", "1"); assert_terminal_field(&stdout, "Ready signed", "0"); } @@ -1901,24 +1739,23 @@ fn transport_outbox_status_read_only_succeeds_for_empty_nostr_profile() { assert_terminal_field(&stdout, "State", "unconfigured"); assert_terminal_field(&stdout, "Profile", "nostr"); assert_terminal_field(&stdout, "Total", "0"); - assert_terminal_field(&stdout, "Preview unavailable", "0"); assert_terminal_field(&stdout, "Deferred", "0"); assert_terminal_field(&stdout, "Publishing", "0"); } #[test] -fn config_get_reticulum_preview_transport_exec_readiness_uses_shared_unavailable_copy() { +fn config_get_reticulum_transport_exec_readiness_uses_shared_unavailable_copy() { let sandbox = RadrootsCliSandbox::new(); sandbox.write_app_config( - "[transport]\nprofile = \"reticulum_preview\"\n\n[transport.reticulum_preview]\nbehavior = \"defer_delivery_plans\"\n", + "[transport]\nprofile = \"reticulum\"\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\n", ); let value = sandbox.json_success(&["--format", "json", "config", "get"]); let result = &value["result"]; assert_eq!(value["operation_id"], "config.get"); - assert_eq!(result["publish"]["transport"], "reticulum_preview"); - assert_eq!(result["publish"]["state"], "preview_unavailable"); + assert_eq!(result["publish"]["transport"], "reticulum"); + assert_eq!(result["publish"]["state"], "unavailable"); assert_eq!(result["publish"]["executable"], false); assert_eq!( result["publish"]["reason"], @@ -1942,7 +1779,7 @@ fn config_get_reticulum_preview_transport_exec_readiness_uses_shared_unavailable } #[test] -fn mesh_status_reports_reticulum_preview_unusable_state_without_fallback() { +fn mesh_status_reports_reticulum_unusable_state_without_fallback() { let sandbox = RadrootsCliSandbox::new(); let value = sandbox.json_success(&["--format", "json", "mesh", "status"]); @@ -1951,10 +1788,13 @@ fn mesh_status_reports_reticulum_preview_unusable_state_without_fallback() { assert_eq!(value["operation_id"], "mesh.status"); assert_eq!(result["transport"], "reticulum"); assert_eq!(result["configured"], false); - assert_eq!(result["implementation"], "preview_unavailable"); + assert_eq!(result["implementation"], "unavailable"); assert_eq!(result["usable_for_delivery"], false); assert_no_removed_transport_status_fields(result); - assert_contains(&result["message"], "Reticulum mesh preview is explicit"); + assert_eq!( + result["message"], + "Reticulum mesh delivery is unavailable until implementation" + ); assert!( !result["message"] .as_str() @@ -1964,7 +1804,7 @@ fn mesh_status_reports_reticulum_preview_unusable_state_without_fallback() { } #[test] -fn mesh_policy_check_reports_structured_preview_denial() { +fn mesh_policy_check_reports_structured_delivery_denial() { let sandbox = RadrootsCliSandbox::new(); let value = sandbox.json_success(&["--format", "json", "mesh", "policy", "check"]); @@ -1972,11 +1812,11 @@ fn mesh_policy_check_reports_structured_preview_denial() { assert_eq!(value["operation_id"], "mesh.policy.check"); assert_eq!(result["scope"], "disabled"); - assert_eq!(result["policy"], "reticulum_preview_delivery"); + assert_eq!(result["policy"], "reticulum_mesh_delivery"); assert_eq!(result["transport"], "reticulum"); assert_eq!(result["usable_for_delivery"], false); assert_eq!(result["decision"], "denied"); - assert_eq!(result["deny_reason"], "preview_unavailable"); + assert_eq!(result["deny_reason"], "delivery_unavailable"); assert_eq!(result["privacy_class"], "public_event"); assert_eq!(result["payload_bytes"], 1); assert_eq!(result["frame_bytes"], 1); @@ -1984,7 +1824,10 @@ fn mesh_policy_check_reports_structured_preview_denial() { assert_eq!(result["max_frame_bytes"], 0); assert_eq!(result["compression"], "disabled"); assert_eq!(result["custom_scopes_enabled"], false); - assert_contains(&result["message"], "Reticulum mesh preview is explicit"); + assert_eq!( + result["message"], + "Reticulum mesh delivery is unavailable until implementation" + ); assert!( !result["message"] .as_str() @@ -1994,8 +1837,10 @@ fn mesh_policy_check_reports_structured_preview_denial() { } #[test] -fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() { +fn transport_source_boundary_rejects_removed_relay_and_daemon_transport_surfaces() { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let removed_daemon_transport_alias = ["radrootsd_", "pro", "xy"].concat(); + let removed_daemon_publish_alias = ["radrootsd.publish_", "pro", "xy.v1"].concat(); assert!( !manifest_dir.join("src/cli/relay.rs").exists(), @@ -2010,8 +1855,8 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() ] { let source = fs::read_to_string(manifest_dir.join(relative_path)).expect("read source"); for forbidden in [ - "\"radrootsd_proxy\"", - "radrootsd.publish_proxy.v1", + removed_daemon_transport_alias.as_str(), + removed_daemon_publish_alias.as_str(), "publish.relays.resolve", "\"publish.event\"", "transport_kinds", @@ -2113,9 +1958,8 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() "RadrootsTransportStatus::new", "RadrootsTransportKind::Reticulum", "transport_runtime_status_view", - "validate_proxy_token_material", - "proxy_token_ready(&validation_config)", - "crate::runtime::sdk::validate_proxy_bearer_token(config)", + "RadrootsTransportCapabilityMaturity::Preview", + "RadrootsTransportCapabilityAvailability::Unavailable", ] { assert!( transport_source.contains(required), @@ -2132,6 +1976,8 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() "pub transport: String,", "pub configured: bool,", "pub implementation: String,", + "pub maturity: String,", + "pub availability: String,", "pub usable_for_delivery: bool,", "pub capabilities: TransportOperationCapabilitiesView,", "pub struct TransportOperationCapabilitiesView", @@ -2235,14 +2081,14 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() ] { assert!( !mesh_policy_view_source.contains(forbidden), - "mesh policy view must not expose preview-success field `{forbidden}`" + "mesh policy view must not expose unavailable-delivery success field `{forbidden}`" ); } let mesh_runtime_source = fs::read_to_string(manifest_dir.join("src/runtime/mesh.rs")).expect("read mesh runtime"); for required in [ - "RadrootsMeshPayloadPolicy::preview_unavailable()", + "RadrootsMeshPayloadPolicy::reticulum_unavailable()", "policy.evaluate(&input)", "decision.usable_for_delivery()", "deny_reason", @@ -2253,7 +2099,7 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() ] { assert!( mesh_runtime_source.contains(required), - "src/runtime/mesh.rs must derive structured preview denial from mesh policy witness `{required}`" + "src/runtime/mesh.rs must derive structured delivery denial from mesh policy witness `{required}`" ); } for forbidden in [ @@ -2267,46 +2113,7 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() ] { assert!( !mesh_runtime_source.contains(forbidden), - "src/runtime/mesh.rs must not imply Reticulum preview success or fallback `{forbidden}`" - ); - } - - let sdk_source = fs::read_to_string(manifest_dir.join("src/runtime/sdk.rs")).expect("read sdk"); - for required in [ - "pub(crate) fn validate_proxy_bearer_token", - ".with_bearer_token(proxy_bearer_token(config)?)", - "proxy transport profile requires a configured token file or token secret id", - "contains unsupported control characters", - ] { - assert!( - sdk_source.contains(required), - "src/runtime/sdk.rs must retain proxy token materialization witness `{required}`" - ); - } - - let main_source = fs::read_to_string(manifest_dir.join("src/main.rs")).expect("read main"); - assert!( - main_source.contains("runtime::transport::proxy_token_ready(config)"), - "src/main.rs must retain routed proxy token readiness validation" - ); - - let core_source = - fs::read_to_string(manifest_dir.join("src/ops/exec/core.rs")).expect("read core"); - assert!( - core_source.contains("crate::runtime::transport::proxy_token_ready(config)"), - "src/ops/exec/core.rs must retain config and health proxy token readiness validation" - ); - - let test_source = fs::read_to_string(file!()).expect("read test source"); - for required in [ - "config_get_proxy_reports_redacted_token_material_failures", - "transport_profile_set_proxy_rejects_unmaterializable_token_sources", - "routed_proxy_operations_validate_token_material_before_sdk_runtime", - "config_get_proxy_with_token_secret_id_reports_ready_transport", - ] { - assert!( - test_source.contains(required), - "tests/target_cli.rs must retain proxy readiness proof `{required}`" + "src/runtime/mesh.rs must not imply Reticulum delivery success or fallback `{forbidden}`" ); } } @@ -2595,606 +2402,6 @@ fn removed_global_flags_are_rejected_publicly() { } #[test] -fn config_get_exposes_proxy_missing_token_state() { - let sandbox = RadrootsCliSandbox::new(); - write_proxy_transport_config(&sandbox, ""); - - let value = sandbox.json_success(&["--format", "json", "config", "get"]); - - assert_eq!(value["operation_id"], "config.get"); - assert_eq!(value["result"]["publish"]["transport"], "proxy"); - assert_eq!( - value["result"]["publish"]["source"], - "user config · transport profile" - ); - assert_eq!(value["result"]["publish"]["transport_family"], "proxy"); - assert_eq!(value["result"]["publish"]["state"], "unconfigured"); - assert_eq!(value["result"]["publish"]["executable"], false); - assert_contains( - &value["result"]["publish"]["reason"], - "configured token file or token secret id", - ); - assert_eq!( - value["result"]["account_resolution"]["status"], - "unresolved" - ); - assert_eq!( - value["result"]["publish"]["provider"]["provider_runtime_id"], - "proxy" - ); - assert_eq!( - value["result"]["write_plane"]["provider_runtime_id"], - "proxy" - ); - assert_eq!( - value["result"]["write_plane"]["binding_model"], - "proxy_transport" - ); - assert_eq!(value["result"]["write_plane"]["state"], "unconfigured"); - assert_eq!(value["result"]["transport"]["profile_id"], "proxy"); - assert_eq!(value["result"]["transport"]["state"], "unconfigured"); - assert_eq!( - value["result"]["transport"]["profile_delivery_usable"], - false - ); - assert_eq!( - value["result"]["transport"]["proxy_url"], - "http://127.0.0.1:7070" - ); - assert!(value["result"]["transport"]["proxy_token_source"].is_null()); - assert!(value["result"]["transport"]["proxy_token_file"].is_null()); - assert!(value["result"]["transport"]["proxy_token_secret_id"].is_null()); - assert_eq!( - value["result"]["actions"][0], - "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID" - ); - assert_eq!( - value["next_actions"][0]["env_var"], - "RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE" - ); -} - -#[test] -fn config_get_proxy_with_token_file_reports_ready_transport() { - let sandbox = RadrootsCliSandbox::new(); - let token_file = proxy_token_file(&sandbox); - write_proxy_transport_config( - &sandbox, - format!( - "token_file = \"{}\"\n", - toml_string(token_file.display().to_string().as_str()) - ) - .as_str(), - ); - - let value = sandbox.json_success(&["--format", "json", "config", "get"]); - assert_eq!(value["operation_id"], "config.get"); - assert_eq!(value["result"]["publish"]["transport"], "proxy"); - assert_eq!(value["result"]["publish"]["state"], "ready"); - assert_eq!(value["result"]["publish"]["executable"], true); - assert_eq!(value["result"]["publish"]["reason"], Value::Null); - assert_eq!(value["result"]["transport"]["profile_id"], "proxy"); - assert_eq!(value["result"]["transport"]["state"], "configured"); - assert_eq!( - value["result"]["transport"]["profile_delivery_usable"], - true - ); - assert_eq!( - value["result"]["transport"]["proxy_token_source"], - "token_file" - ); - assert_eq!( - value["result"]["transport"]["proxy_token_file"], - token_file.display().to_string() - ); - assert!(value["result"]["transport"]["proxy_token_secret_id"].is_null()); - assert!( - !serde_json::to_string(&value) - .expect("json") - .contains("proxy_test_token") - ); - assert_eq!( - value["result"]["actions"] - .as_array() - .expect("actions") - .len(), - 0 - ); -} - -#[test] -fn config_get_proxy_with_token_secret_id_reports_ready_transport() { - let sandbox = RadrootsCliSandbox::new(); - store_test_session_secret(&sandbox, "proxy.publish.token", "proxy_secret_token"); - write_proxy_transport_config(&sandbox, "token_secret_id = \"proxy.publish.token\"\n"); - - let value = sandbox.json_success(&["--format", "json", "config", "get"]); - - assert_eq!(value["operation_id"], "config.get"); - assert_eq!(value["result"]["publish"]["transport"], "proxy"); - assert_eq!(value["result"]["publish"]["state"], "ready"); - assert_eq!(value["result"]["publish"]["executable"], true); - assert_eq!(value["result"]["transport"]["profile_id"], "proxy"); - assert_eq!(value["result"]["transport"]["state"], "configured"); - assert_eq!( - value["result"]["transport"]["profile_delivery_usable"], - true - ); - assert_eq!( - value["result"]["transport"]["proxy_token_source"], - "token_secret_id" - ); - assert_eq!( - value["result"]["transport"]["proxy_token_secret_id"], - "proxy.publish.token" - ); - assert!( - !serde_json::to_string(&value) - .expect("json") - .contains("proxy_secret_token") - ); -} - -#[test] -fn config_get_proxy_reports_redacted_token_material_failures() { - let missing_file = RadrootsCliSandbox::new(); - let missing_path = missing_file.root().join("missing.proxy.token"); - write_proxy_transport_config( - &missing_file, - format!( - "token_file = \"{}\"\n", - toml_string(missing_path.display().to_string().as_str()) - ) - .as_str(), - ); - let missing_file_value = missing_file.json_success(&["--format", "json", "config", "get"]); - assert_eq!( - missing_file_value["result"]["publish"]["state"], - "unconfigured" - ); - assert_contains( - &missing_file_value["result"]["publish"]["reason"], - "failed to read proxy token file", - ); - assert_contains( - &missing_file_value["result"]["transport"]["message"], - missing_path.display().to_string().as_str(), - ); - - let missing_secret = RadrootsCliSandbox::new(); - write_proxy_transport_config( - &missing_secret, - "token_secret_id = \"proxy.publish.missing\"\n", - ); - let missing_secret_value = missing_secret.json_success(&["--format", "json", "config", "get"]); - assert_eq!( - missing_secret_value["result"]["publish"]["state"], - "unconfigured" - ); - assert_contains( - &missing_secret_value["result"]["publish"]["reason"], - "proxy token secret `proxy.publish.missing` was not found", - ); - - let empty_file = RadrootsCliSandbox::new(); - let empty_path = empty_file.root().join("empty.proxy.token"); - fs::write(&empty_path, "\n").expect("write empty proxy token"); - write_proxy_transport_config( - &empty_file, - format!( - "token_file = \"{}\"\n", - toml_string(empty_path.display().to_string().as_str()) - ) - .as_str(), - ); - let empty_value = empty_file.json_success(&["--format", "json", "config", "get"]); - assert_eq!(empty_value["result"]["publish"]["state"], "unconfigured"); - assert_contains( - &empty_value["result"]["publish"]["reason"], - "proxy token file", - ); - assert_contains(&empty_value["result"]["publish"]["reason"], "is empty"); - - let control_file = RadrootsCliSandbox::new(); - let control_path = control_file.root().join("control.proxy.token"); - fs::write(&control_path, "proxy_control\tsecret").expect("write control proxy token"); - write_proxy_transport_config( - &control_file, - format!( - "token_file = \"{}\"\n", - toml_string(control_path.display().to_string().as_str()) - ) - .as_str(), - ); - let control_value = control_file.json_success(&["--format", "json", "config", "get"]); - assert_eq!(control_value["result"]["publish"]["state"], "unconfigured"); - assert_contains( - &control_value["result"]["publish"]["reason"], - "contains unsupported control characters", - ); - assert!( - !serde_json::to_string(&control_value) - .expect("json") - .contains("proxy_control") - ); -} - -#[test] -fn transport_profile_set_proxy_persists_token_file_without_printing_token_material() { - let sandbox = RadrootsCliSandbox::new(); - let token_file = proxy_token_file(&sandbox); - - let value = sandbox.json_success(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - "--proxy-token-file", - token_file.to_string_lossy().as_ref(), - ]); - - assert_eq!(value["operation_id"], "transport.profile.set"); - assert_eq!(value["result"]["profile_id"], "proxy"); - assert_eq!(value["result"]["state"], "configured"); - assert_eq!(value["result"]["profile_delivery_usable"], true); - assert_eq!( - value["result"]["transport_statuses"][0]["transport"], - "proxy" - ); - assert_eq!( - value["result"]["transport_statuses"][0]["implementation"], - "real" - ); - assert_eq!(value["result"]["transport_statuses"][0]["configured"], true); - assert_eq!( - value["result"]["transport_statuses"][0]["usable_for_delivery"], - true - ); - assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false); - assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]); - assert_eq!(value["result"]["proxy_token_source"], "token_file"); - assert_eq!( - value["result"]["proxy_token_file"], - token_file.display().to_string() - ); - assert!(value["result"]["proxy_token_secret_id"].is_null()); - assert!( - !serde_json::to_string(&value) - .expect("json") - .contains("proxy_test_token") - ); - - let get = sandbox.json_success(&["--format", "json", "transport", "profile", "get"]); - assert_eq!(get["result"]["proxy_token_source"], "token_file"); - assert_eq!( - get["result"]["proxy_token_file"], - token_file.display().to_string() - ); - assert!( - !serde_json::to_string(&get) - .expect("json") - .contains("proxy_test_token") - ); -} - -#[test] -fn transport_profile_set_proxy_persists_token_secret_id_without_printing_token_material() { - let sandbox = RadrootsCliSandbox::new(); - let token_file = proxy_token_file(&sandbox); - store_test_session_secret(&sandbox, "proxy.publish.token", "proxy_secret_token"); - - let value = sandbox.json_success(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - "--proxy-token-secret-id", - "proxy.publish.token", - ]); - - assert_eq!(value["operation_id"], "transport.profile.set"); - assert_eq!(value["result"]["profile_id"], "proxy"); - assert_eq!(value["result"]["state"], "configured"); - assert_eq!(value["result"]["profile_delivery_usable"], true); - assert_eq!( - value["result"]["transport_statuses"][0]["transport"], - "proxy" - ); - assert_eq!( - value["result"]["transport_statuses"][0]["implementation"], - "real" - ); - assert_eq!(value["result"]["transport_statuses"][0]["configured"], true); - assert_eq!( - value["result"]["transport_statuses"][0]["usable_for_delivery"], - true - ); - assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false); - assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]); - assert_eq!(value["result"]["proxy_token_source"], "token_secret_id"); - assert!(value["result"]["proxy_token_file"].is_null()); - assert_eq!( - value["result"]["proxy_token_secret_id"], - "proxy.publish.token" - ); - assert!( - !serde_json::to_string(&value) - .expect("json") - .contains("proxy_secret_token") - ); - - let get = sandbox.json_success(&["--format", "json", "transport", "profile", "get"]); - assert_eq!(get["result"]["proxy_token_source"], "token_secret_id"); - assert_eq!( - get["result"]["proxy_token_secret_id"], - "proxy.publish.token" - ); - assert!( - !serde_json::to_string(&get) - .expect("json") - .contains("proxy_secret_token") - ); - assert!(token_file.exists()); -} - -#[test] -fn transport_profile_set_proxy_rejects_missing_and_conflicting_token_sources() { - let sandbox = RadrootsCliSandbox::new(); - let token_file = proxy_token_file(&sandbox); - - let (missing_output, missing_value) = sandbox.json_output(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - ]); - assert!(!missing_output.status.success()); - assert_contains( - &missing_value["errors"][0]["message"], - "requires --proxy-token-file or --proxy-token-secret-id", - ); - - let (conflict_output, conflict_value) = sandbox.json_output(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - "--proxy-token-file", - token_file.to_string_lossy().as_ref(), - "--proxy-token-secret-id", - "proxy.publish.token", - ]); - assert!(!conflict_output.status.success()); - assert_contains( - &conflict_value["errors"][0]["message"], - "cannot set both --proxy-token-file and --proxy-token-secret-id", - ); - assert!( - !serde_json::to_string(&conflict_value) - .expect("json") - .contains("proxy_test_token") - ); -} - -#[test] -fn transport_profile_set_proxy_rejects_unmaterializable_token_sources() { - let missing_file = RadrootsCliSandbox::new(); - let missing_path = missing_file.root().join("missing.profile.token"); - let (missing_file_output, missing_file_value) = missing_file.json_output(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - "--proxy-token-file", - missing_path.to_string_lossy().as_ref(), - ]); - assert!(!missing_file_output.status.success()); - assert_contains( - &missing_file_value["errors"][0]["message"], - "failed to read proxy token file", - ); - - let missing_secret = RadrootsCliSandbox::new(); - let (missing_secret_output, missing_secret_value) = missing_secret.json_output(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - "--proxy-token-secret-id", - "proxy.profile.missing", - ]); - assert!(!missing_secret_output.status.success()); - assert_contains( - &missing_secret_value["errors"][0]["message"], - "proxy token secret `proxy.profile.missing` was not found", - ); - - let empty_file = RadrootsCliSandbox::new(); - let empty_path = empty_file.root().join("empty.profile.token"); - fs::write(&empty_path, "\n").expect("write empty profile token"); - let (empty_output, empty_value) = empty_file.json_output(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - "--proxy-token-file", - empty_path.to_string_lossy().as_ref(), - ]); - assert!(!empty_output.status.success()); - assert_contains(&empty_value["errors"][0]["message"], "is empty"); - - let control_file = RadrootsCliSandbox::new(); - let control_path = control_file.root().join("control.profile.token"); - fs::write(&control_path, "profile_control\tsecret").expect("write control profile token"); - let (control_output, control_value) = control_file.json_output(&[ - "--format", - "json", - "transport", - "profile", - "set", - "--kind", - "proxy", - "--proxy-url", - "http://127.0.0.1:7070", - "--proxy-token-file", - control_path.to_string_lossy().as_ref(), - ]); - assert!(!control_output.status.success()); - assert_contains( - &control_value["errors"][0]["message"], - "contains unsupported control characters", - ); - assert!( - !serde_json::to_string(&control_value) - .expect("json") - .contains("profile_control") - ); -} - -#[test] -fn routed_proxy_operations_validate_token_material_before_sdk_runtime() { - let sandbox = RadrootsCliSandbox::new(); - let missing_path = sandbox.root().join("missing.routed.token"); - write_proxy_transport_config( - &sandbox, - format!( - "token_file = \"{}\"\n", - toml_string(missing_path.display().to_string().as_str()) - ) - .as_str(), - ); - - let (online_output, online_value) = - sandbox.json_output(&["--format", "json", "--online", "sync", "push"]); - assert!(!online_output.status.success()); - assert_eq!(online_value["errors"][0]["code"], "network_unavailable"); - assert_contains( - &online_value["errors"][0]["message"], - "failed to read proxy token file", - ); - - let (routed_output, routed_value) = sandbox.json_output(&[ - "--format", - "json", - "--approval-token", - "approve", - "transport", - "outbox", - "push", - ]); - assert!(!routed_output.status.success()); - assert_eq!(routed_value["errors"][0]["code"], "network_unavailable"); - assert_contains( - &routed_value["errors"][0]["message"], - "failed to read proxy token file", - ); -} - -#[test] -fn config_get_rejects_proxy_transport_config_with_conflicting_token_sources() { - let sandbox = RadrootsCliSandbox::new(); - let token_file = proxy_token_file(&sandbox); - sandbox.write_app_config( - format!( - r#"[transport] -profile = "proxy" - -[transport.proxy] -token_file = "{}" -token_secret_id = "proxy.publish.token" -"#, - toml_string(token_file.display().to_string().as_str()) - ) - .as_str(), - ); - - let (output, value) = sandbox.json_output(&["--format", "json", "config", "get"]); - - assert!(!output.status.success()); - assert_contains( - &value["errors"][0]["message"], - "cannot set both token_file and token_secret_id", - ); - assert!( - !serde_json::to_string(&value) - .expect("json") - .contains("proxy_test_token") - ); -} - -#[test] -fn config_get_marks_proxy_unconfigured_with_incomplete_myc_signer() { - let sandbox = RadrootsCliSandbox::new(); - let token_file = proxy_token_file(&sandbox); - sandbox.write_app_config(&format!( - r#"[transport] -profile = "proxy" - -[transport.proxy] -token_file = "{}" - -[signer] -backend = "myc" - -[[capability_binding]] -capability = "signer.remote_nip46" -provider = "myc" -target_kind = "explicit_endpoint" -target = "http://myc.invalid" -signer_session_ref = "session_ready" -"#, - toml_string(token_file.display().to_string().as_str()) - )); - - let value = sandbox.json_success(&["--format", "json", "config", "get"]); - assert_eq!(value["operation_id"], "config.get"); - assert_eq!(value["result"]["publish"]["transport"], "proxy"); - assert_eq!(value["result"]["publish"]["state"], "unconfigured"); - assert_eq!(value["result"]["publish"]["executable"], false); - assert_contains(&value["result"]["publish"]["reason"], "signer.remote_nip46"); - assert_eq!( - value["result"]["publish"]["provider"]["state"], - "unconfigured" - ); - assert_eq!(value["result"]["actions"][0], "radroots signer status get"); -} - -#[test] fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() { let sandbox = RadrootsCliSandbox::new(); configure_nostr_transport(&sandbox, "ws://127.0.0.1:19001"); @@ -3305,51 +2512,6 @@ fn config_get_marks_relay_publish_unconfigured_with_watch_only_account() { } #[test] -fn health_surfaces_publish_state_under_missing_myc_binding() { - let sandbox = RadrootsCliSandbox::new(); - let missing_myc = sandbox.root().join("bin/missing-myc"); - let token_file = proxy_token_file(&sandbox); - sandbox.write_app_config(&format!( - "[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\ntoken_file = \"{}\"\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n", - toml_string(token_file.display().to_string().as_str()), - toml_string(missing_myc.display().to_string().as_str()) - )); - - let value = sandbox.json_success(&["--format", "json", "health", "status", "get"]); - - assert_eq!(value["operation_id"], "health.status.get"); - assert_eq!(value["result"]["state"], "needs_attention"); - assert_eq!(value["result"]["publish"]["transport"], "proxy"); - assert_eq!(value["result"]["publish"]["executable"], false); - assert_eq!( - value["result"]["publish"]["provider"]["state"], - "unconfigured" - ); - assert_contains( - &value["result"]["publish"]["reason"], - "signer.remote_nip46 binding is missing", - ); - assert_eq!(value["result"]["store"]["state"], "ready"); - assert_eq!( - value["result"]["store"]["source"], - "SDK canonical event store and outbox" - ); - assert_eq!(value["result"]["store"]["canonical_store"], "sdk"); - assert_eq!(value["result"]["signer"]["state"], "unconfigured"); - assert_eq!(value["result"]["actions"][0], "radroots account create"); - assert_eq!(value["result"]["actions"][1], "radroots signer status get"); - assert_eq!( - value["next_actions"][0]["command"], - "radroots account create" - ); - assert_eq!( - value["next_actions"][1]["command"], - "radroots signer status get" - ); - assert_eq!(value["errors"].as_array().expect("errors").len(), 0); -} - -#[test] fn health_status_distinguishes_relay_ready_from_missing_signed_write_account() { let sandbox = RadrootsCliSandbox::new(); configure_nostr_transport(&sandbox, "ws://127.0.0.1:19005"); @@ -3368,35 +2530,11 @@ fn health_status_distinguishes_relay_ready_from_missing_signed_write_account() { ); assert_eq!(value["result"]["store"]["state"], "ready"); assert_eq!(value["result"]["store"]["canonical_store"], "sdk"); - assert_eq!(value["result"]["signer"]["state"], "unconfigured"); - assert_eq!(value["result"]["actions"][0], "radroots account create"); - assert_eq!( - value["next_actions"][0]["command"], - "radroots account create" - ); -} - -#[test] -fn health_check_rejects_proxy_profile_without_token_material() { - let sandbox = RadrootsCliSandbox::new(); - write_proxy_transport_config(&sandbox, ""); - - let (output, value) = sandbox.json_output(&["--format", "json", "health", "check", "run"]); - - assert!(!output.status.success()); - assert_eq!(value["operation_id"], "health.check.run"); - assert_eq!(value["result"], Value::Null); - assert_eq!(value["errors"][0]["code"], "runtime_error"); - assert_contains( - &value["errors"][0]["message"], - "proxy transport profile requires a configured token file or token secret id", - ); + assert_eq!(value["result"]["signer"]["state"], "unconfigured"); + assert_eq!(value["result"]["actions"][0], "radroots account create"); assert_eq!( - value["next_actions"] - .as_array() - .expect("next actions") - .len(), - 0 + value["next_actions"][0]["command"], + "radroots account create" ); } @@ -3469,214 +2607,6 @@ fn farm_readiness_check_reports_mode_specific_publish_gates() { assert_eq!(relay_detail["transport_profile"], "local_only"); assert_eq!(relay_detail["publish_state"], "ready"); assert_eq!(relay_detail["publish_executable"], true); - - let proxy_token_path = proxy_token_file(&sandbox); - sandbox.write_app_config(&format!( - r#"[transport] -profile = "proxy" - -[transport.proxy] -token_file = "{}" - -[[capability_binding]] -capability = "signer.remote_nip46" -provider = "myc" -target_kind = "explicit_endpoint" -target = "http://myc.invalid" -signer_session_ref = "session_test" -"#, - toml_string(proxy_token_path.display().to_string().as_str()) - )); - let output = sandbox - .command() - .args(["--format", "json", "farm", "readiness", "check"]) - .output() - .expect("run proxy farm readiness"); - let proxy_value: Value = serde_json::from_slice(&output.stdout).expect("json output"); - - assert!(output.status.success()); - assert_eq!(proxy_value["operation_id"], "farm.readiness.check"); - assert_contains(&proxy_value["result"]["transport_profile"], "proxy"); - assert_eq!(proxy_value["result"]["publish_state"], "ready"); - assert_eq!(proxy_value["result"]["publish_executable"], true); - assert_eq!(proxy_value["result"]["reason"], Value::Null); - assert_eq!(proxy_value["result"]["actions"][0], "radroots farm publish"); -} - -#[test] -fn proxy_listing_publish_update_and_archive_dry_run_without_direct_publish_relays() { - for operation in ["publish", "update", "archive"] { - let sandbox = RadrootsCliSandbox::new(); - sandbox.json_success(&["--format", "json", "account", "create"]); - let farm = sandbox.json_success(&[ - "--format", - "json", - "farm", - "create", - "--name", - "Proxy Farm", - "--location", - "farmstand", - "--city", - "San Francisco", - "--country", - "US", - "--geohash", - "9q8yy", - "--delivery-method", - "pickup", - ]); - let listing_file = create_listing_draft(&sandbox, format!("proxy-{operation}").as_str()); - make_listing_publishable( - &listing_file, - farm["result"]["config"]["farm_d_tag"] - .as_str() - .expect("farm d tag"), - ); - - let token_file = proxy_token_file(&sandbox); - sandbox.write_app_config(&format!( - "[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\ntoken_file = \"{}\"\n", - toml_string(token_file.display().to_string().as_str()) - )); - - let output = sandbox - .command() - .args([ - "--format", - "json", - "--dry-run", - "listing", - operation, - listing_file.to_string_lossy().as_ref(), - ]) - .output() - .expect("run proxy listing dry run"); - let value: Value = serde_json::from_slice(&output.stdout).expect("json output"); - - assert!(output.status.success()); - assert_eq!(value["operation_id"], format!("listing.{operation}")); - assert_eq!(value["result"]["state"], "dry_run"); - assert_eq!( - value["result"]["source"], - "SDK listing publish · configured signer" - ); - assert_eq!(value["result"]["dry_run"], true); - assert_eq!( - value["result"]["target_transport_endpoints"] - .as_array() - .expect("relays") - .len(), - 0 - ); - assert_contains( - &value["result"]["reason"], - "SDK enqueue and transport push skipped", - ); - } -} - -#[test] -fn proxy_listing_publish_non_dry_run_uses_local_jsonrpc_server() { - let sandbox = RadrootsCliSandbox::new(); - let proxy = ProxyJsonRpcServer::once(Some("proxy_test_token")); - let token_file = proxy_token_file(&sandbox); - sandbox.write_app_config( - format!( - r#"[transport] -profile = "proxy" - -[transport.proxy] -url = "{}" -token_file = "{}" -"#, - toml_string(proxy.endpoint()), - toml_string(token_file.display().to_string().as_str()) - ) - .as_str(), - ); - sandbox.json_success(&["--format", "json", "account", "create"]); - let farm = sandbox.json_success(&[ - "--format", - "json", - "farm", - "create", - "--name", - "Proxy Farm", - "--location", - "farmstand", - "--city", - "San Francisco", - "--country", - "US", - "--geohash", - "9q8yy", - "--delivery-method", - "pickup", - ]); - let listing_file = create_listing_draft(&sandbox, "proxy-live"); - make_listing_publishable( - &listing_file, - farm["result"]["config"]["farm_d_tag"] - .as_str() - .expect("farm d tag"), - ); - - let output = sandbox - .command() - .args([ - "--format", - "json", - "--approval-token", - "approve", - "listing", - "publish", - listing_file.to_string_lossy().as_ref(), - ]) - .output() - .expect("run proxy listing publish"); - let request = proxy.join(); - let value: Value = serde_json::from_slice(&output.stdout).expect("json output"); - - assert!( - output.status.success(), - "stderr `{}` stdout `{}`", - String::from_utf8_lossy(&output.stderr), - String::from_utf8_lossy(&output.stdout) - ); - assert_eq!(value["operation_id"], "listing.publish"); - assert_eq!(value["result"]["state"], "published"); - assert_eq!( - value["result"]["source"], - "SDK listing publish · configured signer" - ); - assert_eq!(value["result"]["dry_run"], false); - let event = proxy_request_raw_event(&request); - assert_eq!(event["id"], value["result"]["event_id"]); - assert_eq!( - request["params"]["target_policy"]["relay_urls"] - .as_array() - .expect("relay urls") - .len(), - 0 - ); - assert!( - request["params"]["idempotency_key"] - .as_str() - .expect("daemon idempotency key") - .contains("-1-") - ); - - let status = sandbox.json_success(&["--format", "json", "sync", "status", "get"]); - assert_eq!( - status["result"]["source"], - "SDK canonical event store and outbox" - ); - assert_eq!(status["result"]["queue"]["pending_count"], 0); - assert_eq!(status["result"]["queue"]["ready_signed_count"], 0); - assert_eq!(status["result"]["queue"]["publishing_count"], 0); - assert_eq!(status["result"]["queue"]["retryable_count"], 0); - assert_eq!(status["result"]["queue"]["failed_terminal_count"], 0); } #[test] @@ -3689,11 +2619,7 @@ fn direct_publish_listing_uses_myc_nip46_sdk_signer() { let user_keys = nostr_keys_from_identity(&user_identity); let remote_keys = nostr_keys_from_identity(&remote_identity); let remote_pubkey = remote_keys.public_key().to_hex(); - let relay = Nip46RelayServer::new( - remote_keys.clone(), - user_keys, - Nip46RelayFinish::ProductPublish, - ); + let relay = Nip46RelayServer::new(remote_keys.clone(), user_keys); let relay_endpoint = relay.endpoint().to_owned(); let public_identity_file = write_public_identity_profile(&sandbox, "myc-direct-user", &user_public); @@ -3779,6 +2705,8 @@ relay_urls = ["{}"] "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000100", "listing", "publish", listing_file.to_string_lossy().as_ref(), @@ -3818,127 +2746,6 @@ relay_urls = ["{}"] } #[test] -fn proxy_listing_publish_uses_myc_nip46_sdk_signer() { - let sandbox = RadrootsCliSandbox::new(); - let user_identity = identity_secret(93); - let client_identity = identity_secret(94); - let remote_identity = identity_secret(95); - let user_public = user_identity.to_public(); - let user_keys = nostr_keys_from_identity(&user_identity); - let remote_keys = nostr_keys_from_identity(&remote_identity); - let remote_pubkey = remote_keys.public_key().to_hex(); - let relay = Nip46RelayServer::new( - remote_keys.clone(), - user_keys, - Nip46RelayFinish::SignResponse, - ); - let proxy = ProxyJsonRpcServer::once(Some("proxy_test_token")); - let token_file = proxy_token_file(&sandbox); - let public_identity_file = - write_public_identity_profile(&sandbox, "myc-proxy-user", &user_public); - let imported = sandbox.json_success(&[ - "--format", - "json", - "--approval-token", - "approve", - "account", - "import", - "--default", - public_identity_file.to_string_lossy().as_ref(), - ]); - let account_id = imported["result"]["account"]["id"] - .as_str() - .expect("account id"); - store_test_session_secret( - &sandbox, - "session_ready", - client_identity.secret_key_hex().as_str(), - ); - let config = format!( - r#"[transport] -profile = "proxy" - -[transport.proxy] -url = "{}" -token_file = "{}" - -{}"#, - toml_string(proxy.endpoint()), - toml_string(token_file.display().to_string().as_str()), - myc_nip46_config( - remote_pubkey.as_str(), - relay.endpoint(), - account_id, - "session_ready", - ), - ); - sandbox.write_app_config(config.as_str()); - let farm = sandbox.json_success(&[ - "--format", - "json", - "farm", - "create", - "--name", - "Myc Proxy Farm", - "--location", - "farmstand", - "--city", - "San Francisco", - "--country", - "US", - "--geohash", - "9q8yy", - "--delivery-method", - "pickup", - ]); - let listing_file = create_listing_draft(&sandbox, "myc-proxy-live"); - make_listing_publishable( - &listing_file, - farm["result"]["config"]["farm_d_tag"] - .as_str() - .expect("farm d tag"), - ); - - let output = sandbox - .command() - .args([ - "--format", - "json", - "--approval-token", - "approve", - "listing", - "publish", - listing_file.to_string_lossy().as_ref(), - ]) - .output() - .expect("run myc proxy listing publish"); - let value: Value = serde_json::from_slice(&output.stdout).expect("json output"); - - assert!( - output.status.success(), - "stderr `{}` stdout `{}`", - String::from_utf8_lossy(&output.stderr), - String::from_utf8_lossy(&output.stdout) - ); - let report = relay.join(); - let request = proxy.join(); - assert_eq!(value["operation_id"], "listing.publish"); - assert_eq!(value["result"]["state"], "published"); - assert_eq!( - value["result"]["source"], - "SDK listing publish · configured signer" - ); - assert!(report.connection_count >= 1); - assert!(report.req_count >= 1); - assert_eq!(report.sign_request_count, 1); - assert_eq!(report.published_events.len(), 0); - let event = proxy_request_raw_event(&request); - assert_eq!(event["kind"], KIND_LISTING); - assert_eq!(event["pubkey"], user_public.public_key_hex); - assert_eq!(event["id"], value["result"]["event_id"]); -} - -#[test] fn listing_update_publish_attempts_direct_publish_with_approval() { let sandbox = RadrootsCliSandbox::new(); sandbox.json_success(&["--format", "json", "account", "create"]); @@ -3974,6 +2781,8 @@ fn listing_update_publish_attempts_direct_publish_with_approval() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000103", "listing", "update", listing_file.to_string_lossy().as_ref(), @@ -4407,25 +3216,6 @@ fn next_actions_mirror_result_actions_for_json_and_ndjson() { terminal["payload"]["next_actions"][0]["command"], "radroots store init" ); - - for args in [&["--format", "ndjson", "config", "get"][..]] { - let proxy = RadrootsCliSandbox::new(); - write_proxy_transport_config(&proxy, ""); - let output = proxy.command().args(args).output().expect("run ndjson"); - let frames = ndjson_from_stdout(&output); - let terminal = frames.last().expect("terminal ndjson frame"); - - assert!(output.status.success(), "{args:?}"); - assert!( - terminal["payload"]["next_actions"] - .as_array() - .expect("next actions") - .iter() - .any(|action| action["description"] - == "configure RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_FILE or RADROOTS_CLI_TRANSPORT_PROXY_TOKEN_SECRET_ID"), - "{args:?}" - ); - } } #[test] @@ -5039,6 +3829,8 @@ fn offline_listing_publish_enqueues_sdk_outbox_without_direct_publish_push() { "--offline", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000104", "listing", "publish", listing_file.to_string_lossy().as_ref(), @@ -5062,7 +3854,7 @@ fn offline_listing_publish_enqueues_sdk_outbox_without_direct_publish_push() { assert!( sandbox .root() - .join("data/apps/cli/replica/sdk/outbox.sqlite") + .join("data/apps/cli/replica/sdk/runtime.sqlite") .exists() ); assert_eq!( @@ -5099,7 +3891,7 @@ fn listing_publish_idempotency_conflict_maps_sdk_partial_mutation_recovery() { let listing_file = create_listing_draft(&sandbox, "idem-conflict"); make_listing_publishable(&listing_file, farm_d_tag); let relay = "ws://127.0.0.1:9"; - let idempotency_key = "listing-idem-conflict"; + let idempotency_key = "01890f3c-7f2a-7b01-8000-000000000102"; configure_nostr_transport(&sandbox, relay); sandbox.json_success(&[ @@ -5480,7 +4272,7 @@ fn removed_transport_profile_flag_is_rejected() { let sandbox = RadrootsCliSandbox::new(); let output = sandbox .command() - .args(["--publish-transport", "proxy", "sync", "push"]) + .args(["--publish-transport", "radrootsd", "sync", "push"]) .output() .expect("run removed publish transport flag"); let stderr = String::from_utf8(output.stderr).expect("utf8 stderr"); @@ -5580,8 +4372,7 @@ fn store_backup_and_restore_use_sdk_canonical_store() { status["result"]["derived_projection"]["source"], "local derived projection cache" ); - assert!(sdk_root.join("event_store.sqlite").exists()); - assert!(sdk_root.join("outbox.sqlite").exists()); + assert!(sdk_root.join("runtime.sqlite").exists()); let derived_projection_init = sandbox.json_success(&["--format", "json", "store", "init"]); assert_eq!(derived_projection_init["operation_id"], "store.init"); @@ -5679,10 +4470,10 @@ fn store_backup_and_restore_use_sdk_canonical_store() { true ); assert!( - backup["result"]["manifest"]["source_paths"]["event_store_path"] + backup["result"]["manifest"]["source_paths"]["runtime_path"] .as_str() - .expect("source event store path") - .contains("data/apps/cli/replica/sdk/event_store.sqlite") + .expect("source runtime path") + .contains("data/apps/cli/replica/sdk/runtime.sqlite") ); assert!(!event_store_file.ends_with("replica.sqlite")); @@ -5747,19 +4538,18 @@ fn store_backup_and_restore_use_sdk_canonical_store() { ); assert_eq!(restored["result"]["restore_kind"], "sdk_canonical"); assert_eq!(restored["result"]["canonical_store"], "sdk"); - assert!(restore_destination.join("event_store.sqlite").exists()); - assert!(restore_destination.join("outbox.sqlite").exists()); + assert!(restore_destination.join("runtime.sqlite").exists()); assert_eq!( restored["result"]["restored_event_store_file"], restore_destination - .join("event_store.sqlite") + .join("runtime.sqlite") .to_string_lossy() .to_string() ); assert_eq!( restored["result"]["restored_outbox_file"], restore_destination - .join("outbox.sqlite") + .join("runtime.sqlite") .to_string_lossy() .to_string() ); @@ -7489,6 +6279,8 @@ fn farm_publish_uses_sdk_outbox_without_legacy_signed_event_records() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000101", "farm", "publish", ]); @@ -7561,6 +6353,8 @@ fn listing_publish_failure_uses_sdk_outbox_without_legacy_runtime_store_record() "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000106", "listing", "publish", listing_file.to_string_lossy().as_ref(), @@ -7634,6 +6428,8 @@ fn sync_push_sdk_outbox_failure_reports_network_unavailable() { "--offline", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000105", "listing", "publish", listing_file.to_string_lossy().as_ref(), @@ -7709,7 +6505,7 @@ fn sync_push_ignores_derived_projection_pending_queue_for_sdk_canonical_push() { DERIVED_PROJECTION_SYNC_PUSH_FARM_D_TAG, selected_pubkey, ); - let executor = SqliteExecutor::open(sandbox.replica_store_path()).expect("open replica"); + let executor = SqlxSqliteExecutor::open(sandbox.replica_store_path()).expect("open replica"); let derived_projection_batch = radroots_replica_pending_publish_batch(&executor) .expect("derived projection pending batch"); assert!(derived_projection_batch.pending_count > 0); @@ -9152,39 +7948,21 @@ fn buyer_side_order_writes_reject_conflicting_account_override_for_local_draft() .expect("drift account id"); configure_nostr_transport(&sandbox, "ws://127.0.0.1:9"); - for (operation_id, command) in [ - ( - "trade.revision.accept", - vec![ - "--format", - "json", - "--dry-run", - "--account-id", - drift_account_id, - "trade", - "revision", - "accept", - order_id.as_str(), - "--revision-id", - "rev_pending", - ], - ), - ( - "trade.cancel", - vec![ - "--format", - "json", - "--dry-run", - "--account-id", - drift_account_id, - "trade", - "cancel", - order_id.as_str(), - "--reason", - "changed plans", - ], - ), - ] { + for (operation_id, command) in [( + "trade.cancel", + vec![ + "--format", + "json", + "--dry-run", + "--account-id", + drift_account_id, + "trade", + "cancel", + order_id.as_str(), + "--reason", + "changed plans", + ], + )] { let (output, value) = sandbox.json_output(command.as_slice()); assert!(!output.status.success(), "{operation_id} should fail"); @@ -9212,6 +7990,8 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_replica_freshness_ "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000200", "trade", "submit", order_id.as_str(), @@ -9258,6 +8038,8 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_listing_state_gate "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000201", "trade", "submit", order_id.as_str(), @@ -9284,6 +8066,8 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_listing_event_gate "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000202", "trade", "submit", order_id.as_str(), @@ -9337,6 +8121,8 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_quantity_gate() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000203", "trade", "submit", order_id, @@ -9364,6 +8150,8 @@ fn order_submit_non_dry_run_uses_sdk_relay_validation_without_bin_gate() { "json", "--approval-token", "approve", + "--idempotency-key", + "01890f3c-7f2a-7b01-8000-000000000204", "trade", "submit", order_id.as_str(),