commit c685656cd9604417e9ce965770fa66740e595283
parent c83384f32fa144e1ba51160e7b7f63531248aba3
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Jul 2026 03:53:01 +0000
transport: wire proxy auth into SDK publish
- Resolve proxy bearer tokens from token files or configured secret ids before building SDK profiles.
- Reuse the selected account secret vault for proxy token secret lookup without serializing token bytes.
- Update CLI proxy publish tests to require bearer auth and speak transport.publish.event.
- Validate with cargo fmt, cargo check, focused CLI tests, and retired daemon-field scans.
Diffstat:
4 files changed, 145 insertions(+), 36 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3835,13 +3835,6 @@ dependencies = [
]
[[package]]
-name = "radroots_publish_proxy_protocol"
-version = "0.1.0-alpha.2"
-dependencies = [
- "serde",
-]
-
-[[package]]
name = "radroots_replica_db"
version = "0.1.0-alpha.2"
dependencies = [
@@ -3929,11 +3922,11 @@ dependencies = [
"radroots_nostr_connect",
"radroots_nostr_signer",
"radroots_outbox",
- "radroots_publish_proxy_protocol",
"radroots_runtime_paths",
"radroots_trade",
"radroots_transport",
"radroots_transport_nostr",
+ "radroots_transport_publish_protocol",
"radroots_transport_reticulum",
"reqwest",
"serde",
@@ -4033,6 +4026,13 @@ dependencies = [
]
[[package]]
+name = "radroots_transport_publish_protocol"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "serde",
+]
+
+[[package]]
name = "radroots_transport_reticulum"
version = "0.1.0-alpha.2"
dependencies = [
diff --git a/src/runtime/account.rs b/src/runtime/account.rs
@@ -775,6 +775,13 @@ fn secret_backend_resolution_error(error: SecretBackendResolutionError) -> Runti
}
}
+pub(crate) fn account_secret_vault(
+ config: &RuntimeConfig,
+) -> Result<Arc<dyn RadrootsSecretVault>, RuntimeError> {
+ let resolved = resolve_secret_backend(config).map_err(secret_backend_resolution_error)?;
+ secret_vault_for_backend(config, resolved.backend, HOST_VAULT_SERVICE_NAME)
+}
+
fn secret_vault_for_backend(
config: &RuntimeConfig,
backend: RadrootsSecretBackend,
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -1,3 +1,4 @@
+use std::fs;
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
@@ -644,12 +645,64 @@ fn sdk_transport_profile(config: &RuntimeConfig) -> Result<TransportProfile, Run
ReticulumPreviewProfile::preview_unavailable().with_behavior(behavior),
))
}
- TransportProfileKind::Proxy => Ok(TransportProfile::proxy(ProxyProfile::new(
- config.transport.proxy.url.clone(),
- ))),
+ TransportProfileKind::Proxy => {
+ let mut profile = ProxyProfile::new(config.transport.proxy.url.clone());
+ if let Some(token) = proxy_bearer_token(config)? {
+ profile = profile.with_bearer_token(token);
+ }
+ Ok(TransportProfile::proxy(profile))
+ }
}
}
+fn proxy_bearer_token(config: &RuntimeConfig) -> Result<Option<String>, RuntimeError> {
+ if let Some(path) = config.transport.proxy.token_file.as_ref() {
+ let token = fs::read_to_string(path).map_err(|error| {
+ RuntimeError::Config(format!(
+ "failed to read proxy token file {}: {error}",
+ path.display()
+ ))
+ })?;
+ return normalize_proxy_bearer_token(
+ token.as_str(),
+ format!("proxy token file {}", path.display()).as_str(),
+ )
+ .map(Some);
+ }
+
+ if let Some(secret_id) = config.transport.proxy.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}"
+ ))
+ })?;
+ let token = token.ok_or_else(|| {
+ RuntimeError::Config(format!("proxy token secret `{secret_id}` was not found"))
+ })?;
+ return normalize_proxy_bearer_token(
+ token.as_str(),
+ format!("proxy token secret `{secret_id}`").as_str(),
+ )
+ .map(Some);
+ }
+
+ Ok(None)
+}
+
+fn normalize_proxy_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")));
+ }
+ if token.bytes().any(|byte| byte.is_ascii_control()) {
+ return Err(RuntimeError::Config(format!(
+ "{source} contains unsupported control characters"
+ )));
+ }
+ Ok(token.to_owned())
+}
+
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
@@ -658,7 +711,7 @@ mod tests {
use std::time::Duration;
use radroots_authority::RadrootsEventSigner;
- use radroots_sdk::{SdkStorageKind, StorageStatusRequest};
+ use radroots_sdk::{ProxyAuth, SdkStorageKind, StorageStatusRequest};
use radroots_secret_vault::RadrootsSecretBackend;
use tempfile::tempdir;
@@ -1126,6 +1179,63 @@ mod tests {
}
#[test]
+ fn maps_proxy_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 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_eq!(profile.endpoint_url(), "http://127.0.0.1:7070");
+ assert_eq!(
+ profile.auth(),
+ &ProxyAuth::BearerToken("proxy-file-token".to_owned())
+ );
+ }
+
+ #[test]
+ fn maps_proxy_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());
+ let vault = account::account_secret_vault(&config).expect("account vault");
+ vault
+ .store_secret("proxy_token", "proxy-secret-token")
+ .expect("store proxy 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");
+ };
+ assert_eq!(
+ profile.auth(),
+ &ProxyAuth::BearerToken("proxy-secret-token".to_owned())
+ );
+ }
+
+ #[test]
+ fn proxy_token_resolution_rejects_empty_or_header_unsafe_tokens() {
+ assert!(matches!(
+ normalize_proxy_bearer_token(" \n", "proxy token"),
+ Err(RuntimeError::Config(message)) if message.contains("empty")
+ ));
+ assert!(matches!(
+ normalize_proxy_bearer_token("proxy\nsecret", "proxy token"),
+ Err(RuntimeError::Config(message)) if message.contains("control characters")
+ ));
+ }
+
+ #[test]
fn maps_localhost_ws_relays_to_localhost_sdk_policy() {
let root = tempdir().expect("tempdir");
let config = sample_config(root.path(), vec!["ws://127.0.0.1:8080".to_owned()]);
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -519,23 +519,15 @@ fn handle_proxy_connection(mut stream: TcpStream, expected_token: Option<&str>)
}
let request: Value = serde_json::from_str(body.as_str()).expect("proxy json");
assert_eq!(request["jsonrpc"], "2.0");
- assert_eq!(request["method"], "publish.event");
+ assert_eq!(request["method"], "transport.publish.event");
let event = &request["params"]["event"];
- let relays = request["params"]["relays"]
- .as_array()
- .cloned()
- .unwrap_or_default();
- let relay_results = relays
- .iter()
- .map(|relay| {
- json!({
- "relay_url": relay,
- "source": "request",
- "attempted": true,
- "outcome_kind": "accepted"
- })
- })
- .collect::<Vec<_>>();
+ 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"],
@@ -549,15 +541,15 @@ fn handle_proxy_connection(mut stream: TcpStream, expected_token: Option<&str>)
"event_id": event["id"],
"pubkey": event["pubkey"],
"event_kind": event["kind"],
- "relay_policy": request["params"]["relay_policy"],
+ "target_policy": request["params"]["target_policy"],
"delivery_policy": request["params"]["delivery_policy"],
- "relay_count": relay_results.len(),
- "acknowledged_count": relay_results.len(),
+ "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,
- "relays": relay_results
+ "targets": [target_result]
}
}
});
@@ -1853,7 +1845,7 @@ fn proxy_listing_publish_update_and_archive_dry_run_without_direct_publish_relay
#[test]
fn proxy_listing_publish_non_dry_run_uses_local_jsonrpc_server() {
let sandbox = RadrootsCliSandbox::new();
- let proxy = ProxyJsonRpcServer::once(None);
+ let proxy = ProxyJsonRpcServer::once(Some("proxy_test_token"));
let token_file = proxy_token_file(&sandbox);
sandbox.write_app_config(
format!(
@@ -1930,9 +1922,9 @@ token_file = "{}"
value["result"]["event_id"]
);
assert_eq!(
- request["params"]["relays"]
+ request["params"]["target_policy"]["relay_urls"]
.as_array()
- .expect("relays")
+ .expect("relay urls")
.len(),
0
);
@@ -2105,7 +2097,7 @@ fn proxy_listing_publish_uses_myc_nip46_sdk_signer() {
user_keys,
Nip46RelayFinish::SignResponse,
);
- let proxy = ProxyJsonRpcServer::once(None);
+ 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);