myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit b5034dd8ade932849ecb0a0b182b9529aa7f07ab
parent 40ddd75b3cdaa15856bb9f473bf773c14a9c8e19
Author: triesap <tyson@radroots.org>
Date:   Thu, 26 Mar 2026 20:05:12 +0000

delivery: route control publishes through outbox

Diffstat:
Msrc/control.rs | 413+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/transport/nip46.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/nip46_e2e.rs | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 544 insertions(+), 51 deletions(-)

diff --git a/src/control.rs b/src/control.rs @@ -5,15 +5,16 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectResponse, RadrootsNostrConnectUri, }; use radroots_nostr_signer::prelude::{ - RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthorizationOutcome, - RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord, - RadrootsNostrSignerPendingRequest, RadrootsNostrSignerRequestId, + RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerConnectionId, + RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerRequestId, + RadrootsNostrSignerWorkflowId, }; use serde::Serialize; use crate::app::MycRuntime; use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord}; use crate::error::MycError; +use crate::outbox::{MycDeliveryOutboxKind, MycDeliveryOutboxRecord}; use crate::transport::{MycNip46Handler, MycNostrTransport, MycPublishOutcome}; #[derive(Debug, Serialize)] @@ -41,10 +42,18 @@ pub async fn authorize_auth_challenge( .signer_context() .policy() .ensure_authorize_auth_challenge_allowed(&connection)?; - let outcome = manager.authorize_auth_challenge(connection_id)?; - let replayed_request_id = replay_authorized_request(runtime, &outcome).await?; + let workflow = manager.begin_auth_replay_publish_finalization(connection_id)?; + let replayed_request_id = + replay_authorized_request(runtime, &connection.connection_id, &workflow.workflow_id) + .await?; + let connection = runtime + .signer_manager()? + .get_connection(connection_id)? + .ok_or_else(|| { + MycError::InvalidOperation(format!("connection `{connection_id}` was not found")) + })?; Ok(MycAuthorizedReplayOutput { - connection: outcome.connection, + connection, replayed_request_id, }) } @@ -148,17 +157,55 @@ pub async fn accept_client_uri( RadrootsNostrConnectResponse::ConnectSecretEcho(client_uri.secret), )?; let response_relays = merge_relays(&client_uri.relays, &preferred_relays); - let publish_outcome = match MycNostrTransport::publish_once( + let workflow = manager.begin_connect_secret_publish_finalization(&connection.connection_id)?; + let event = match event.sign_with_keys(runtime.signer_identity().keys()) { + Ok(event) => event, + Err(error) => { + return Err(cancel_connect_accept_workflow_on_error( + runtime, + &workflow.workflow_id, + MycError::InvalidOperation(format!( + "failed to sign connect accept response event: {error}" + )), + )); + } + }; + let outbox_record = match build_control_outbox_record( + MycDeliveryOutboxKind::ConnectAcceptPublish, + event.clone(), + &response_relays, + Some(&connection.connection_id), + Some(response_request_id.as_str()), + Some(&workflow.workflow_id), + ) { + Ok(record) => record, + Err(error) => { + return Err(cancel_connect_accept_workflow_on_error( + runtime, + &workflow.workflow_id, + error, + )); + } + }; + if let Err(error) = runtime.delivery_outbox_store().enqueue(&outbox_record) { + return Err(cancel_connect_accept_workflow_on_error( + runtime, + &workflow.workflow_id, + error, + )); + } + let publish_outcome = match MycNostrTransport::publish_event_once( runtime.signer_identity(), &response_relays, &runtime.config().transport, "connect accept response publish", - event, + &event, ) .await { Ok(outcome) => outcome, Err(error) => { + let error = mark_outbox_publish_failed(runtime, &outbox_record, error); runtime.record_operation_audit(&record_publish_failure( MycOperationAuditKind::ConnectAcceptPublish, Some(&connection.connection_id), @@ -166,9 +213,63 @@ pub async fn accept_client_uri( response_relays.len(), &error, )); - return Err(error); + return Err(cancel_connect_accept_workflow_on_error( + runtime, + &workflow.workflow_id, + error, + )); } }; + if let Err(error) = manager.mark_publish_workflow_published(&workflow.workflow_id) { + record_post_publish_failure( + runtime, + MycOperationAuditKind::ConnectAcceptPublish, + Some(&connection.connection_id), + Some(response_request_id.as_str()), + &publish_outcome, + format!("failed to mark connect-accept publish workflow as published: {error}"), + ); + return Err(error.into()); + } + if let Err(error) = runtime + .delivery_outbox_store() + .mark_published_pending_finalize(&outbox_record.job_id, publish_outcome.attempt_count) + { + record_post_publish_failure( + runtime, + MycOperationAuditKind::ConnectAcceptPublish, + Some(&connection.connection_id), + Some(response_request_id.as_str()), + &publish_outcome, + format!("failed to persist connect-accept outbox published state: {error}"), + ); + return Err(error); + } + if let Err(error) = manager.finalize_publish_workflow(&workflow.workflow_id) { + record_post_publish_failure( + runtime, + MycOperationAuditKind::ConnectAcceptPublish, + Some(&connection.connection_id), + Some(response_request_id.as_str()), + &publish_outcome, + format!("failed to finalize connect-accept publish workflow: {error}"), + ); + return Err(error.into()); + } + if let Err(error) = runtime + .delivery_outbox_store() + .mark_finalized(&outbox_record.job_id) + { + record_post_publish_failure( + runtime, + MycOperationAuditKind::ConnectAcceptPublish, + Some(&connection.connection_id), + Some(response_request_id.as_str()), + &publish_outcome, + format!("failed to finalize connect-accept outbox job: {error}"), + ); + return Err(error); + } record_publish_audit( runtime, MycOperationAuditKind::ConnectAcceptPublish, @@ -177,7 +278,6 @@ pub async fn accept_client_uri( Some(response_request_id.as_str()), &publish_outcome, ); - let _ = manager.mark_connect_secret_consumed(&connection.connection_id)?; Ok(MycAcceptedConnectionOutput { connection: runtime @@ -213,9 +313,14 @@ pub fn parse_permission_values( async fn replay_authorized_request( runtime: &MycRuntime, - outcome: &RadrootsNostrSignerAuthorizationOutcome, + connection_id: &RadrootsNostrSignerConnectionId, + workflow_id: &RadrootsNostrSignerWorkflowId, ) -> Result<Option<String>, MycError> { - let Some(pending_request) = outcome.pending_request.clone() else { + let manager = runtime.signer_manager()?; + let workflow = manager.get_publish_workflow(workflow_id)?.ok_or_else(|| { + MycError::InvalidOperation(format!("publish workflow `{workflow_id}` was not found")) + })?; + let Some(pending_request) = workflow.pending_request.clone() else { return Ok(None); }; let transport = match runtime.transport() { @@ -224,25 +329,38 @@ async fn replay_authorized_request( let error = MycError::InvalidOperation( "transport.enabled must be true to replay authorized requests".to_owned(), ); - return Err(restore_pending_auth_challenge_on_error( + return Err(cancel_auth_replay_workflow_on_error( runtime, - &outcome.connection.connection_id, - pending_request, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), error, )); } }; let handler = MycNip46Handler::new(runtime.signer_context(), transport.relays().to_vec()); - let handled_request = match handler.handle_request( - outcome.connection.client_public_key, - pending_request.request_message.clone(), - ) { + let evaluation = match manager.evaluate_auth_replay_publish_workflow(workflow_id) { + Ok(evaluation) => evaluation, + Err(error) => { + return Err(cancel_auth_replay_workflow_on_error( + runtime, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), + error.into(), + )); + } + }; + let handled_request = match handler + .handle_authorized_request_evaluation(pending_request.request_message.clone(), evaluation) + { Ok(handled_request) => handled_request, Err(error) => { - return Err(restore_pending_auth_challenge_on_error( + return Err(cancel_auth_replay_workflow_on_error( runtime, - &outcome.connection.connection_id, - pending_request, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), error, )); } @@ -252,86 +370,196 @@ async fn replay_authorized_request( let error = MycError::InvalidOperation( "authorized auth replay did not produce a response".to_owned(), ); - return Err(restore_pending_auth_challenge_on_error( + return Err(cancel_auth_replay_workflow_on_error( runtime, - &outcome.connection.connection_id, - pending_request, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), error, )); }; + if consume_connect_secret_for.is_some() { + return Err(cancel_auth_replay_workflow_on_error( + runtime, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), + MycError::InvalidOperation( + "auth replay unexpectedly requested connect-secret finalization".to_owned(), + ), + )); + } let event = match handler.build_response_event( - outcome.connection.client_public_key, + manager + .get_connection(connection_id)? + .ok_or_else(|| { + MycError::InvalidOperation(format!("connection `{connection_id}` was not found")) + })? + .client_public_key, pending_request.request_message.id.clone(), response, ) { Ok(event) => event, Err(error) => { - return Err(restore_pending_auth_challenge_on_error( + return Err(cancel_auth_replay_workflow_on_error( runtime, - &outcome.connection.connection_id, - pending_request, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), error, )); } }; - let publish_relays = if outcome.connection.relays.is_empty() { + let connection = manager.get_connection(connection_id)?.ok_or_else(|| { + MycError::InvalidOperation(format!("connection `{connection_id}` was not found")) + })?; + let event = match event.sign_with_keys(runtime.signer_identity().keys()) { + Ok(event) => event, + Err(error) => { + return Err(cancel_auth_replay_workflow_on_error( + runtime, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), + MycError::InvalidOperation(format!( + "failed to sign authorized auth replay response event: {error}" + )), + )); + } + }; + let publish_relays = if connection.relays.is_empty() { transport.relays().to_vec() } else { - outcome.connection.relays.clone() + connection.relays.clone() }; - let publish_outcome = match MycNostrTransport::publish_once( + let outbox_record = match build_control_outbox_record( + MycDeliveryOutboxKind::AuthReplayPublish, + event.clone(), + &publish_relays, + Some(connection_id), + Some(&pending_request.request_message.id), + Some(workflow_id), + ) { + Ok(record) => record, + Err(error) => { + return Err(cancel_auth_replay_workflow_on_error( + runtime, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), + error, + )); + } + }; + if let Err(error) = runtime.delivery_outbox_store().enqueue(&outbox_record) { + return Err(cancel_auth_replay_workflow_on_error( + runtime, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), + error, + )); + } + let publish_outcome = match MycNostrTransport::publish_event_once( runtime.signer_identity(), &publish_relays, &runtime.config().transport, "authorized auth replay publish", - event, + &event, ) .await { Ok(publish_outcome) => publish_outcome, Err(error) => { + let error = mark_outbox_publish_failed(runtime, &outbox_record, error); runtime.record_operation_audit(&record_publish_failure( MycOperationAuditKind::AuthReplayPublish, - Some(&outcome.connection.connection_id), + Some(connection_id), Some(pending_request.request_message.id.as_str()), publish_relays.len(), &error, )); - return Err(restore_pending_auth_challenge_on_error( + return Err(cancel_auth_replay_workflow_on_error( runtime, - &outcome.connection.connection_id, - pending_request, + connection_id, + workflow_id, + Some(&pending_request.request_message.id), error, )); } }; + if let Err(error) = manager.mark_publish_workflow_published(workflow_id) { + record_post_publish_failure( + runtime, + MycOperationAuditKind::AuthReplayPublish, + Some(connection_id), + Some(pending_request.request_message.id.as_str()), + &publish_outcome, + format!("failed to mark auth replay publish workflow as published: {error}"), + ); + return Err(error.into()); + } + if let Err(error) = runtime + .delivery_outbox_store() + .mark_published_pending_finalize(&outbox_record.job_id, publish_outcome.attempt_count) + { + record_post_publish_failure( + runtime, + MycOperationAuditKind::AuthReplayPublish, + Some(connection_id), + Some(pending_request.request_message.id.as_str()), + &publish_outcome, + format!("failed to persist auth replay outbox published state: {error}"), + ); + return Err(error); + } + if let Err(error) = manager.finalize_publish_workflow(workflow_id) { + record_post_publish_failure( + runtime, + MycOperationAuditKind::AuthReplayPublish, + Some(connection_id), + Some(pending_request.request_message.id.as_str()), + &publish_outcome, + format!("failed to finalize auth replay publish workflow: {error}"), + ); + return Err(error.into()); + } + if let Err(error) = runtime + .delivery_outbox_store() + .mark_finalized(&outbox_record.job_id) + { + record_post_publish_failure( + runtime, + MycOperationAuditKind::AuthReplayPublish, + Some(connection_id), + Some(pending_request.request_message.id.as_str()), + &publish_outcome, + format!("failed to finalize auth replay outbox job: {error}"), + ); + return Err(error); + } record_publish_audit( runtime, MycOperationAuditKind::AuthReplayPublish, MycOperationAuditOutcome::Succeeded, - Some(&outcome.connection.connection_id), + Some(connection_id), Some(pending_request.request_message.id.as_str()), &publish_outcome, ); - if let Some(connection_id) = consume_connect_secret_for { - runtime - .signer_manager()? - .mark_connect_secret_consumed(&connection_id)?; - } Ok(Some(pending_request.request_message.id.clone())) } -fn restore_pending_auth_challenge_on_error( +fn cancel_auth_replay_workflow_on_error( runtime: &MycRuntime, connection_id: &RadrootsNostrSignerConnectionId, - pending_request: RadrootsNostrSignerPendingRequest, + workflow_id: &RadrootsNostrSignerWorkflowId, + request_id: Option<&str>, error: MycError, ) -> MycError { let summary = publish_failure_summary(&error); - let request_id = pending_request.request_message.id.clone(); match runtime.signer_manager().and_then(|manager| { manager - .restore_pending_auth_challenge(connection_id, pending_request.clone()) + .cancel_publish_workflow(workflow_id) .map_err(Into::into) }) { Ok(_) => { @@ -339,7 +567,7 @@ fn restore_pending_auth_challenge_on_error( MycOperationAuditKind::AuthReplayRestore, MycOperationAuditOutcome::Restored, Some(connection_id), - Some(request_id.as_str()), + request_id, error .publish_rejection_counts() .map(|(relay_count, _)| relay_count) @@ -348,7 +576,7 @@ fn restore_pending_auth_challenge_on_error( .publish_rejection_counts() .map(|(_, acknowledged)| acknowledged) .unwrap_or_default(), - format!("restored pending auth challenge after replay failure: {summary}"), + format!("preserved pending auth challenge after replay failure: {summary}"), ); if let ( Some(delivery_policy), @@ -369,7 +597,66 @@ fn restore_pending_auth_challenge_on_error( error } Err(restore_error) => MycError::InvalidOperation(format!( - "{error}; additionally failed to restore pending auth challenge: {restore_error}" + "{error}; additionally failed to cancel auth replay publish workflow: {restore_error}" + )), + } +} + +fn cancel_connect_accept_workflow_on_error( + runtime: &MycRuntime, + workflow_id: &RadrootsNostrSignerWorkflowId, + error: MycError, +) -> MycError { + match runtime.signer_manager().and_then(|manager| { + manager + .cancel_publish_workflow(workflow_id) + .map(|_| ()) + .map_err(Into::into) + }) { + Ok(()) => error, + Err(cancel_error) => MycError::InvalidOperation(format!( + "{error}; additionally failed to cancel connect-accept publish workflow: {cancel_error}" + )), + } +} + +fn build_control_outbox_record( + kind: MycDeliveryOutboxKind, + event: radroots_nostr::prelude::RadrootsNostrEvent, + relay_urls: &[nostr::RelayUrl], + connection_id: Option<&RadrootsNostrSignerConnectionId>, + request_id: Option<&str>, + workflow_id: Option<&RadrootsNostrSignerWorkflowId>, +) -> Result<MycDeliveryOutboxRecord, MycError> { + let relay_urls = relay_urls.to_vec(); + let mut record = MycDeliveryOutboxRecord::new(kind, event, relay_urls)?; + if let Some(connection_id) = connection_id { + record = record.with_connection_id(connection_id); + } + if let Some(request_id) = request_id { + record = record.with_request_id(request_id.to_owned()); + } + if let Some(workflow_id) = workflow_id { + record = record.with_signer_publish_workflow_id(workflow_id); + } + Ok(record) +} + +fn mark_outbox_publish_failed( + runtime: &MycRuntime, + outbox_record: &MycDeliveryOutboxRecord, + error: MycError, +) -> MycError { + let publish_attempt_count = error.publish_attempt_count().unwrap_or_default(); + let summary = publish_failure_summary(&error); + match runtime.delivery_outbox_store().mark_failed( + &outbox_record.job_id, + publish_attempt_count, + &summary, + ) { + Ok(_) => error, + Err(outbox_error) => MycError::InvalidOperation(format!( + "{error}; additionally failed to persist publish failure to the delivery outbox: {outbox_error}" )), } } @@ -400,6 +687,32 @@ fn record_publish_audit( ); } +fn record_post_publish_failure( + runtime: &MycRuntime, + operation: MycOperationAuditKind, + connection_id: Option<&RadrootsNostrSignerConnectionId>, + request_id: Option<&str>, + publish_outcome: &MycPublishOutcome, + summary: impl Into<String>, +) { + runtime.record_operation_audit( + &MycOperationAuditRecord::new( + operation, + MycOperationAuditOutcome::Rejected, + connection_id, + request_id, + publish_outcome.relay_count, + publish_outcome.acknowledged_relay_count, + summary.into(), + ) + .with_delivery_details( + publish_outcome.delivery_policy, + publish_outcome.required_acknowledged_relay_count, + publish_outcome.attempt_count, + ), + ); +} + fn publish_failure_summary(error: &MycError) -> String { error .publish_rejection_details() diff --git a/src/transport/nip46.rs b/src/transport/nip46.rs @@ -385,6 +385,97 @@ impl MycNip46Handler { } } + pub(crate) fn handle_authorized_request_evaluation( + &self, + request_message: RadrootsNostrConnectRequestMessage, + evaluation: RadrootsNostrSignerRequestEvaluation, + ) -> Result<MycNip46HandledRequest, MycError> { + let connection_id = Some(evaluation.connection.connection_id.clone()); + Ok(match request_message.request.clone() { + RadrootsNostrConnectRequest::SignEvent(unsigned_event) => match evaluation.action { + RadrootsNostrSignerRequestAction::Denied { reason } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + RadrootsNostrConnectResponse::Error { + result: None, + error: reason, + }, + ) + } + RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url), + ) + } + RadrootsNostrSignerRequestAction::Allowed { .. } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + self.sign_event_response(unsigned_event)?, + ) + } + }, + RadrootsNostrConnectRequest::Nip04Encrypt { .. } + | RadrootsNostrConnectRequest::Nip04Decrypt { .. } + | RadrootsNostrConnectRequest::Nip44Encrypt { .. } + | RadrootsNostrConnectRequest::Nip44Decrypt { .. } => match evaluation.action { + RadrootsNostrSignerRequestAction::Denied { reason } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + RadrootsNostrConnectResponse::Error { + result: None, + error: reason, + }, + ) + } + RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url), + ) + } + RadrootsNostrSignerRequestAction::Allowed { .. } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + self.crypto_response(request_message.request)?, + ) + } + }, + RadrootsNostrConnectRequest::GetPublicKey + | RadrootsNostrConnectRequest::Ping + | RadrootsNostrConnectRequest::SwitchRelays => match evaluation.action { + RadrootsNostrSignerRequestAction::Denied { reason } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + RadrootsNostrConnectResponse::Error { + result: None, + error: reason, + }, + ) + } + RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url), + ) + } + RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => { + MycNip46HandledRequest::respond_for_connection( + connection_id, + response_from_hint(&evaluation.connection, response_hint)?, + ) + } + }, + other => MycNip46HandledRequest::respond_for_connection( + connection_id, + RadrootsNostrConnectResponse::Error { + result: None, + error: format!("method `{}` is not implemented yet", other.method()), + }, + ), + }) + } + fn evaluate_request_with_policy( &self, connection: &RadrootsNostrSignerConnectionRecord, diff --git a/tests/nip46_e2e.rs b/tests/nip46_e2e.rs @@ -1876,6 +1876,25 @@ async fn connect_accept_retries_without_consuming_secret_until_publish_succeeds( .relay_outcome_summary .contains("blocked by test relay") ); + let outbox_records = wait_for_delivery_outbox_records(&runtime, |records| { + records.len() >= 1 && records[0].status == MycDeliveryOutboxStatus::Failed + }) + .await?; + assert_eq!( + outbox_records[0].kind, + MycDeliveryOutboxKind::ConnectAcceptPublish + ); + assert_eq!( + outbox_records[0].request_id.as_deref(), + operation_audit[0].request_id.as_deref() + ); + assert!(outbox_records[0].signer_publish_workflow_id.is_some()); + assert!( + runtime + .signer_manager()? + .list_publish_workflows()? + .is_empty() + ); let accepted = control::accept_client_uri(&runtime, &client_uri).await?; assert_eq!(accepted.response_request_id.len(), 36); @@ -1921,6 +1940,27 @@ async fn connect_accept_retries_without_consuming_secret_until_publish_succeeds( .relay_outcome_summary .contains("1/1 relays acknowledged publish") ); + let outbox_records = wait_for_delivery_outbox_records(&runtime, |records| { + records.len() >= 2 && records[1].status == MycDeliveryOutboxStatus::Finalized + }) + .await?; + assert_eq!( + outbox_records[1].kind, + MycDeliveryOutboxKind::ConnectAcceptPublish + ); + assert_eq!( + outbox_records[1].request_id.as_deref(), + Some(accepted.response_request_id.as_str()) + ); + assert!(outbox_records[1].published_at_unix.is_some()); + assert!(outbox_records[1].finalized_at_unix.is_some()); + assert!(outbox_records[1].signer_publish_workflow_id.is_some()); + assert!( + runtime + .signer_manager()? + .list_publish_workflows()? + .is_empty() + ); let consumed = control::accept_client_uri(&runtime, &client_uri) .await @@ -2088,6 +2128,21 @@ async fn connect_accept_rejects_when_quorum_delivery_policy_is_not_met() -> Test Some(2) ); assert_eq!(operation_audit[0].publish_attempt_count, Some(1)); + let outbox_records = wait_for_delivery_outbox_records(&runtime, |records| { + records.len() >= 1 && records[0].status == MycDeliveryOutboxStatus::Failed + }) + .await?; + assert_eq!( + outbox_records[0].kind, + MycDeliveryOutboxKind::ConnectAcceptPublish + ); + assert!(outbox_records[0].signer_publish_workflow_id.is_some()); + assert!( + runtime + .signer_manager()? + .list_publish_workflows()? + .is_empty() + ); Ok(()) } @@ -2272,7 +2327,23 @@ async fn auth_replay_restores_pending_request_until_publish_succeeds() -> TestRe assert!( operation_audit[1] .relay_outcome_summary - .contains("restored pending auth challenge") + .contains("preserved pending auth challenge") + ); + let outbox_records = wait_for_delivery_outbox_records(&runtime, |records| { + records.len() >= 1 && records[0].status == MycDeliveryOutboxStatus::Failed + }) + .await?; + assert_eq!( + outbox_records[0].kind, + MycDeliveryOutboxKind::AuthReplayPublish + ); + assert_eq!(outbox_records[0].request_id.as_deref(), Some("auth-ping")); + assert!(outbox_records[0].signer_publish_workflow_id.is_some()); + assert!( + runtime + .signer_manager()? + .list_publish_workflows()? + .is_empty() ); let replayed = control::authorize_auth_challenge(&runtime, &connection.connection_id).await?; @@ -2321,6 +2392,24 @@ async fn auth_replay_restores_pending_request_until_publish_succeeds() -> TestRe .relay_outcome_summary .contains("1/1 relays acknowledged publish") ); + let outbox_records = wait_for_delivery_outbox_records(&runtime, |records| { + records.len() >= 2 && records[1].status == MycDeliveryOutboxStatus::Finalized + }) + .await?; + assert_eq!( + outbox_records[1].kind, + MycDeliveryOutboxKind::AuthReplayPublish + ); + assert_eq!(outbox_records[1].request_id.as_deref(), Some("auth-ping")); + assert!(outbox_records[1].published_at_unix.is_some()); + assert!(outbox_records[1].finalized_at_unix.is_some()); + assert!(outbox_records[1].signer_publish_workflow_id.is_some()); + assert!( + runtime + .signer_manager()? + .list_publish_workflows()? + .is_empty() + ); Ok(()) }