commit 06dd38368fab990b701b402eea3d603e2a8dc9f1
parent 9f334e6f420900509ca02d0562925b3277e6d6cb
Author: triesap <tyson@radroots.org>
Date: Sun, 24 May 2026 18:24:25 +0000
cli: harden app order validation
- route app-order list/export/submit readiness through the shared supported payload validator
- fail closed for stale, unsupported, malformed, and conflicting app-order records
- cover supported and fail-closed app-order states with deterministic target CLI tests
- validate with cargo check, focused app-order tests, nix check, formatting, and diff hygiene
Diffstat:
3 files changed, 334 insertions(+), 65 deletions(-)
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -1501,7 +1501,9 @@ impl OrderAppRecordExportView {
pub fn disposition(&self) -> CommandDisposition {
match self.state.as_str() {
"missing" => CommandDisposition::NotFound,
- "invalid" | "stale" | "unsupported" => CommandDisposition::ValidationFailed,
+ "conflict" | "invalid" | "stale" | "unsupported" => {
+ CommandDisposition::ValidationFailed
+ }
"error" => CommandDisposition::InternalError,
_ => CommandDisposition::Success,
}
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -49,7 +49,7 @@ use radroots_events_codec::trade::{
use radroots_events_codec::wire::WireEventParts;
use radroots_local_events::{
BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecord, LocalRecordFamily,
- LocalRecordStatus, SourceRuntime, validate_buyer_order_request_local_work_payload,
+ LocalRecordStatus, SourceRuntime, validate_supported_buyer_order_request_local_work_payload,
};
use radroots_nostr::prelude::{
RadrootsNostrEvent, RadrootsNostrFilter, radroots_event_from_nostr, radroots_nostr_filter_tag,
@@ -714,47 +714,6 @@ pub fn app_record_export(
});
};
- if let Some(current_record) = current_app_order_record_for(config, &record)?
- && current_record.record_id != record.record_id
- {
- let order_id = app_order_record_order_id(&record);
- return Ok(OrderAppRecordExportView {
- state: "stale".to_owned(),
- source: ORDER_APP_RECORD_SOURCE.to_owned(),
- record_id: args.record_id.clone(),
- dry_run: config.output.dry_run,
- file: args
- .output
- .as_ref()
- .map(|path| path.display().to_string())
- .unwrap_or_default(),
- valid: false,
- order_id: order_id.clone(),
- listing_addr: record.listing_addr.clone(),
- listing_event_id: None,
- buyer_account_id: record.owner_account_id.clone(),
- buyer_pubkey: record.owner_pubkey.clone(),
- buyer_actor_source: None,
- seller_pubkey: None,
- issues: vec![issue_with_code(
- "app_order_stale",
- "record_id",
- format!(
- "app-authored local order record `{}` was superseded by `{}`",
- record.record_id, current_record.record_id
- ),
- )],
- reason: Some(format!(
- "app-authored local order record `{}` was superseded by current record `{}`",
- record.record_id, current_record.record_id
- )),
- actions: vec![
- format!("radroots order app export {}", current_record.record_id),
- "radroots order app list".to_owned(),
- ],
- });
- }
-
let app_order = load_app_order_record_from_record(config, record)?;
let mut issues = source_and_document_issues(config, &app_order)?;
if !issues.is_empty() {
@@ -9499,6 +9458,25 @@ fn current_app_order_record_for(
}))
}
+fn app_order_conflicting_record_ids_for(
+ config: &RuntimeConfig,
+ record: &LocalEventRecord,
+) -> Result<Vec<String>, RuntimeError> {
+ if app_order_record_order_id(record).is_none() {
+ return Ok(Vec::new());
+ }
+ let key = app_order_record_current_key(record);
+ let mut record_ids = app_order_local_records(config)?
+ .into_iter()
+ .filter(|candidate| candidate.record_id != record.record_id)
+ .filter(|candidate| app_order_record_current_key(candidate) == key)
+ .map(|candidate| candidate.record_id)
+ .collect::<Vec<_>>();
+ record_ids.sort();
+ record_ids.dedup();
+ Ok(record_ids)
+}
+
fn load_app_order_record_for_lookup(
config: &RuntimeConfig,
lookup: &str,
@@ -9599,28 +9577,44 @@ fn app_order_record_source_issues(
"app-authored order record is not marked current",
));
}
- if current && let Err(error) = validate_buyer_order_request_local_work_payload(payload) {
+ if payload["currentness"]["record_id"].as_str() != Some(record.record_id.as_str()) {
issues.push(issue_with_code(
"invalid_app_order_record",
- "local_work_json",
- error.to_string(),
- ));
- }
- if payload["support_status"]["state"].as_str() != Some("supported") {
- issues.push(issue_with_code(
- "app_order_unsupported",
- "support_status.state",
- "app-authored order record is not marked supported",
+ "currentness.record_id",
+ "app-authored order record currentness id does not match the shared record id",
));
}
- if let Some(support_issues) = payload["support_status"]["issues"].as_array() {
- for support_issue in support_issues {
- if let Some(support_issue) = support_issue.as_str() {
- issues.push(issue_with_code(
- "app_order_unsupported",
- "support_status.issues",
- format!("app order support issue: {support_issue}"),
- ));
+ if current {
+ match validate_supported_buyer_order_request_local_work_payload(payload) {
+ Ok(_) => {}
+ Err(error) => {
+ let support_state = payload["support_status"]["state"].as_str();
+ let support_issues = payload["support_status"]["issues"]
+ .as_array()
+ .cloned()
+ .unwrap_or_default();
+ if support_state == Some("unsupported") {
+ issues.push(issue_with_code(
+ "app_order_unsupported",
+ "support_status.state",
+ "app-authored order record is not marked supported",
+ ));
+ for support_issue in support_issues {
+ if let Some(support_issue) = support_issue.as_str() {
+ issues.push(issue_with_code(
+ "app_order_unsupported",
+ "support_status.issues",
+ format!("app order support issue: {support_issue}"),
+ ));
+ }
+ }
+ } else {
+ issues.push(issue_with_code(
+ "invalid_app_order_record",
+ "local_work_json",
+ error.to_string(),
+ ));
+ }
}
}
}
@@ -9636,6 +9630,17 @@ fn app_order_record_source_issues(
),
));
}
+ let conflicting_record_ids = app_order_conflicting_record_ids_for(config, record)?;
+ if !conflicting_record_ids.is_empty() {
+ issues.push(issue_with_code(
+ "app_order_conflict",
+ "order_id",
+ format!(
+ "app-authored order id conflicts with other shared records: {}",
+ conflicting_record_ids.join(", ")
+ ),
+ ));
+ }
Ok(issues)
}
@@ -9738,11 +9743,22 @@ fn placeholder_app_order_document(record: &LocalEventRecord) -> OrderDraftDocume
}
fn app_order_export_failure_state(issues: &[OrderIssueView]) -> &'static str {
- if issues.iter().any(|issue| issue.code == "app_order_stale") {
+ if issues
+ .iter()
+ .any(|issue| issue.code == "app_order_conflict")
+ {
+ "conflict"
+ } else if issues.iter().any(|issue| issue.code == "app_order_stale") {
"stale"
- } else if issues.iter().any(|issue| {
- issue.code == "app_order_unsupported" || issue.code == "invalid_app_order_record"
- }) {
+ } else if issues
+ .iter()
+ .any(|issue| issue.code == "invalid_app_order_record")
+ {
+ "invalid"
+ } else if issues
+ .iter()
+ .any(|issue| issue.code == "app_order_unsupported")
+ {
"unsupported"
} else {
"invalid"
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -514,6 +514,34 @@ fn seed_app_order_record_variant(
support_issues: Vec<&str>,
) -> String {
let record_id = format!("app:local_work:order_request:{order_id}");
+ seed_app_order_record_variant_with_record_id(
+ sandbox,
+ account_id,
+ buyer_pubkey,
+ seller_pubkey,
+ order_id,
+ listing_addr,
+ listing_event_id,
+ record_id,
+ current,
+ support_state,
+ support_issues,
+ )
+}
+
+fn seed_app_order_record_variant_with_record_id(
+ sandbox: &RadrootsCliSandbox,
+ account_id: &str,
+ buyer_pubkey: &str,
+ seller_pubkey: &str,
+ order_id: &str,
+ listing_addr: &str,
+ listing_event_id: &str,
+ record_id: String,
+ current: bool,
+ support_state: &str,
+ support_issues: Vec<&str>,
+) -> String {
let support_issues = support_issues
.into_iter()
.map(|issue| Value::String(issue.to_owned()))
@@ -4508,6 +4536,229 @@ fn order_app_records_fail_closed_when_not_current_or_supported() {
}
#[test]
+fn order_app_records_fail_closed_when_unsupported() {
+ let sandbox = RadrootsCliSandbox::new();
+ let account = sandbox.json_success(&["--format", "json", "account", "create"]);
+ let account_id = account["result"]["account"]["id"]
+ .as_str()
+ .expect("account id");
+ let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+ let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
+ .as_str()
+ .expect("buyer pubkey");
+ let seller_pubkey = identity_public(75).public_key_hex;
+ let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
+ let listing_event_id = "3".repeat(64);
+ let order_id = "018f47a8-7b2c-7000-8000-000000000013";
+ let record_id = seed_app_order_record_variant(
+ &sandbox,
+ account_id,
+ buyer_pubkey,
+ seller_pubkey.as_str(),
+ order_id,
+ listing_addr.as_str(),
+ listing_event_id.as_str(),
+ true,
+ "unsupported",
+ vec!["seller_pubkey_required"],
+ );
+
+ let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
+ assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
+ assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
+ assert_eq!(app_list["result"]["records"][0]["exportable"], false);
+ assert!(
+ app_list["result"]["records"][0]["reason"]
+ .as_str()
+ .expect("unsupported reason")
+ .contains("not marked supported")
+ );
+
+ let export_path = sandbox.root().join("unsupported-app-order.toml");
+ let export_path_arg = export_path.to_string_lossy();
+ let (export_output, export) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "order",
+ "app",
+ "export",
+ record_id.as_str(),
+ "--output",
+ export_path_arg.as_ref(),
+ ]);
+ assert!(!export_output.status.success());
+ assert_eq!(export["operation_id"], "order.app.export");
+ assert_eq!(export["errors"][0]["detail"]["state"], "unsupported");
+ assert_eq!(
+ export["errors"][0]["detail"]["issues"][0]["code"],
+ "app_order_unsupported"
+ );
+ assert!(!export_path.exists());
+
+ let (submit_output, submit) =
+ sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
+ assert!(!submit_output.status.success());
+ assert_eq!(
+ submit["errors"][0]["detail"]["issues"][0]["code"],
+ "app_order_unsupported"
+ );
+}
+
+#[test]
+fn order_app_records_fail_closed_when_supported_record_is_malformed() {
+ let sandbox = RadrootsCliSandbox::new();
+ let account = sandbox.json_success(&["--format", "json", "account", "create"]);
+ let account_id = account["result"]["account"]["id"]
+ .as_str()
+ .expect("account id");
+ let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+ let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
+ .as_str()
+ .expect("buyer pubkey");
+ let seller_pubkey = identity_public(75).public_key_hex;
+ let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
+ let listing_event_id = "3".repeat(64);
+ let order_id = "018f47a8-7b2c-7000-8000-000000000014";
+ let record_id = seed_app_order_record_variant(
+ &sandbox,
+ account_id,
+ buyer_pubkey,
+ seller_pubkey.as_str(),
+ order_id,
+ listing_addr.as_str(),
+ listing_event_id.as_str(),
+ true,
+ "supported",
+ vec!["unit_price_required"],
+ );
+
+ let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
+ assert_eq!(app_list["result"]["records"][0]["record_id"], record_id);
+ assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
+ assert_eq!(app_list["result"]["records"][0]["exportable"], false);
+ assert!(
+ app_list["result"]["records"][0]["reason"]
+ .as_str()
+ .expect("malformed reason")
+ .contains("support_status.issues")
+ );
+
+ let export_path = sandbox.root().join("malformed-app-order.toml");
+ let export_path_arg = export_path.to_string_lossy();
+ let (export_output, export) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "order",
+ "app",
+ "export",
+ record_id.as_str(),
+ "--output",
+ export_path_arg.as_ref(),
+ ]);
+ assert!(!export_output.status.success());
+ assert_eq!(export["operation_id"], "order.app.export");
+ assert_eq!(export["errors"][0]["detail"]["state"], "invalid");
+ assert_eq!(
+ export["errors"][0]["detail"]["issues"][0]["code"],
+ "invalid_app_order_record"
+ );
+ assert!(!export_path.exists());
+
+ let (submit_output, submit) =
+ sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
+ assert!(!submit_output.status.success());
+ assert_eq!(submit["operation_id"], "order.submit");
+ assert_eq!(
+ submit["errors"][0]["detail"]["issues"][0]["code"],
+ "invalid_app_order_record"
+ );
+}
+
+#[test]
+fn order_app_records_fail_closed_when_order_id_conflicts() {
+ let sandbox = RadrootsCliSandbox::new();
+ let account = sandbox.json_success(&["--format", "json", "account", "create"]);
+ let account_id = account["result"]["account"]["id"]
+ .as_str()
+ .expect("account id");
+ let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+ let buyer_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
+ .as_str()
+ .expect("buyer pubkey");
+ let seller_pubkey = identity_public(76).public_key_hex;
+ let listing_addr = format!("30402:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAQ");
+ let listing_event_id = "4".repeat(64);
+ let order_id = "018f47a8-7b2c-7000-8000-000000000015";
+ let first_record_id = seed_app_order_record(
+ &sandbox,
+ account_id,
+ buyer_pubkey,
+ seller_pubkey.as_str(),
+ order_id,
+ listing_addr.as_str(),
+ listing_event_id.as_str(),
+ );
+ let conflicting_record_id = format!("app:local_work:order_request:{order_id}:conflict");
+ seed_app_order_record_variant_with_record_id(
+ &sandbox,
+ account_id,
+ buyer_pubkey,
+ seller_pubkey.as_str(),
+ order_id,
+ listing_addr.as_str(),
+ listing_event_id.as_str(),
+ conflicting_record_id.clone(),
+ true,
+ "supported",
+ Vec::new(),
+ );
+
+ let app_list = sandbox.json_success(&["--format", "json", "order", "app", "list"]);
+ assert_eq!(app_list["result"]["count"], 1);
+ assert_eq!(
+ app_list["result"]["records"][0]["record_id"],
+ conflicting_record_id
+ );
+ assert_eq!(app_list["result"]["records"][0]["ready_for_submit"], false);
+ assert_eq!(app_list["result"]["records"][0]["exportable"], false);
+ assert!(
+ app_list["result"]["records"][0]["reason"]
+ .as_str()
+ .expect("conflict reason")
+ .contains(first_record_id.as_str())
+ );
+
+ let export_path = sandbox.root().join("conflicting-app-order.toml");
+ let export_path_arg = export_path.to_string_lossy();
+ let (export_output, export) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "order",
+ "app",
+ "export",
+ conflicting_record_id.as_str(),
+ "--output",
+ export_path_arg.as_ref(),
+ ]);
+ assert!(!export_output.status.success());
+ assert_eq!(export["operation_id"], "order.app.export");
+ assert_eq!(export["errors"][0]["detail"]["state"], "conflict");
+ assert_eq!(
+ export["errors"][0]["detail"]["issues"][0]["code"],
+ "app_order_conflict"
+ );
+ assert!(!export_path.exists());
+
+ let (submit_output, submit) =
+ sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
+ assert!(!submit_output.status.success());
+ assert_eq!(
+ submit["errors"][0]["detail"]["issues"][0]["code"],
+ "app_order_conflict"
+ );
+}
+
+#[test]
fn farm_publish_writes_acknowledged_signed_outbox_records() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);