commit 78eba8ad069b8012d1b40286083548a3899a1bc3
parent 4b72b37d429104cffa4e32aa196ceb97f8a38ff5
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Mar 2026 00:34:38 +0000
transport: ignore consumed connect secrets
- add typed NIP-46 request outcomes so consumed secret replays can be ignored without publishing a response
- persist one-shot connect secret consumption only after successful response publish so failed publishes can retry safely
- apply the same consumed secret semantics to auth replay and client nostrconnect acceptance flows
- validate with cargo fmt --check, cargo test, cargo check --locked, and cargo test --locked
Diffstat:
| M | src/cli.rs | | | 58 | +++++++++++++++++++++++++++++++++++++--------------------- |
| M | src/transport/nip46.rs | | | 327 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- |
2 files changed, 300 insertions(+), 85 deletions(-)
diff --git a/src/cli.rs b/src/cli.rs
@@ -265,10 +265,13 @@ async fn replay_authorized_request(
)
})?;
let handler = MycNip46Handler::new(runtime.signer_context(), transport.relays().to_vec());
- let response = handler.handle_request(
+ let handled_request = handler.handle_request(
outcome.connection.client_public_key,
pending_request.request_message.clone(),
)?;
+ let Some((response, consume_connect_secret_for)) = handled_request.into_publish_parts() else {
+ return Ok(None);
+ };
let event = handler.build_response_event(
outcome.connection.client_public_key,
pending_request.request_message.id.clone(),
@@ -286,6 +289,11 @@ async fn replay_authorized_request(
event,
)
.await?;
+ 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()))
}
@@ -320,31 +328,38 @@ async fn accept_client_uri(
requested_permissions: client_uri.metadata.requested_permissions.clone(),
};
let manager = runtime.signer_manager()?;
- let proposal = match manager.evaluate_connect_request(client_uri.client_public_key, request)? {
- radroots_nostr_signer::prelude::RadrootsNostrSignerConnectEvaluation::ExistingConnection(_) => {
- return Err(MycError::InvalidOperation(
- "connect secret is already bound to an existing connection".to_owned(),
- ));
+ let connection = match manager.evaluate_connect_request(client_uri.client_public_key, request)? {
+ radroots_nostr_signer::prelude::RadrootsNostrSignerConnectEvaluation::ExistingConnection(
+ connection,
+ ) => {
+ if connection.connect_secret_is_consumed() {
+ return Err(MycError::InvalidOperation(
+ "connect secret has already been consumed by a successful connection"
+ .to_owned(),
+ ));
+ }
+ connection
}
radroots_nostr_signer::prelude::RadrootsNostrSignerConnectEvaluation::RegistrationRequired(
proposal,
- ) => proposal,
+ ) => {
+ let draft = proposal
+ .into_connection_draft(runtime.user_public_identity())
+ .with_relays(preferred_relays.clone())
+ .with_approval_requirement(runtime.signer_context().connection_approval_requirement());
+ let connection = manager.register_connection(draft)?;
+ if runtime.signer_context().connection_approval_requirement()
+ == RadrootsNostrSignerApprovalRequirement::NotRequired
+ {
+ let _ = manager.set_granted_permissions(
+ &connection.connection_id,
+ connection.requested_permissions.clone(),
+ )?;
+ }
+ connection
+ }
};
- let draft = proposal
- .into_connection_draft(runtime.user_public_identity())
- .with_relays(preferred_relays.clone())
- .with_approval_requirement(runtime.signer_context().connection_approval_requirement());
- let connection = manager.register_connection(draft)?;
- if runtime.signer_context().connection_approval_requirement()
- == RadrootsNostrSignerApprovalRequirement::NotRequired
- {
- let _ = manager.set_granted_permissions(
- &connection.connection_id,
- connection.requested_permissions.clone(),
- )?;
- }
-
let handler = MycNip46Handler::new(runtime.signer_context(), preferred_relays.clone());
let response_request_id = RadrootsNostrSignerRequestId::new_v7().into_string();
let event = handler.build_response_event(
@@ -360,6 +375,7 @@ async fn accept_client_uri(
event,
)
.await?;
+ let _ = manager.mark_connect_secret_consumed(&connection.connection_id)?;
Ok(MycAcceptedConnectionOutput {
connection: runtime
diff --git a/src/transport/nip46.rs b/src/transport/nip46.rs
@@ -11,9 +11,9 @@ use radroots_nostr_connect::prelude::{
RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
};
use radroots_nostr_signer::prelude::{
- RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectionRecord,
- RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestResponseHint,
- RadrootsNostrSignerSessionLookup,
+ RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectionId,
+ RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerRequestAction,
+ RadrootsNostrSignerRequestResponseHint, RadrootsNostrSignerSessionLookup,
};
use tokio::sync::broadcast;
@@ -32,6 +32,15 @@ pub struct MycNip46Service {
transport: MycNostrTransport,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum MycNip46HandledRequest {
+ Respond {
+ response: RadrootsNostrConnectResponse,
+ consume_connect_secret_for: Option<RadrootsNostrSignerConnectionId>,
+ },
+ Ignore,
+}
+
impl MycNip46Handler {
pub fn new(signer: MycSignerContext, relays: Vec<RadrootsNostrRelayUrl>) -> Self {
Self { signer, relays }
@@ -88,11 +97,11 @@ impl MycNip46Handler {
.tags(vec![RadrootsNostrTag::public_key(client_public_key)]))
}
- pub fn handle_request(
+ pub(crate) fn handle_request(
&self,
client_public_key: RadrootsNostrPublicKey,
request_message: RadrootsNostrConnectRequestMessage,
- ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ ) -> Result<MycNip46HandledRequest, MycError> {
match request_message.request.clone() {
RadrootsNostrConnectRequest::Connect { secret, .. } => {
self.handle_connect_request(client_public_key, request_message.request, secret)
@@ -111,13 +120,29 @@ impl MycNip46Handler {
| RadrootsNostrConnectRequest::SwitchRelays => {
self.handle_base_request(client_public_key, request_message)
}
- _ => Ok(RadrootsNostrConnectResponse::Error {
- result: None,
- error: format!(
- "method `{}` is not implemented yet",
- request_message.request.method()
- ),
- }),
+ _ => Ok(MycNip46HandledRequest::respond(
+ RadrootsNostrConnectResponse::Error {
+ result: None,
+ error: format!(
+ "method `{}` is not implemented yet",
+ request_message.request.method()
+ ),
+ },
+ )),
+ }
+ }
+
+ #[cfg(test)]
+ fn handle_request_response(
+ &self,
+ client_public_key: RadrootsNostrPublicKey,
+ request_message: RadrootsNostrConnectRequestMessage,
+ ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ match self.handle_request(client_public_key, request_message)? {
+ MycNip46HandledRequest::Respond { response, .. } => Ok(response),
+ MycNip46HandledRequest::Ignore => Err(MycError::InvalidOperation(
+ "request was ignored without a response".to_owned(),
+ )),
}
}
@@ -126,13 +151,20 @@ impl MycNip46Handler {
client_public_key: RadrootsNostrPublicKey,
request: RadrootsNostrConnectRequest,
secret: Option<String>,
- ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ ) -> Result<MycNip46HandledRequest, MycError> {
let manager = self.signer.load_signer_manager()?;
let evaluation = manager.evaluate_connect_request(client_public_key, request)?;
match evaluation {
- RadrootsNostrSignerConnectEvaluation::ExistingConnection(_) => {
- Ok(connect_response(secret))
+ RadrootsNostrSignerConnectEvaluation::ExistingConnection(connection) => {
+ if secret.is_some() && connection.connect_secret_is_consumed() {
+ tracing::debug!(
+ connection_id = %connection.connection_id,
+ "ignoring reused consumed NIP-46 connect secret"
+ );
+ return Ok(MycNip46HandledRequest::Ignore);
+ }
+ Ok(connect_response_outcome(&connection, secret))
}
RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => {
let draft = proposal
@@ -150,7 +182,7 @@ impl MycNip46Handler {
granted_permissions,
)?;
}
- Ok(connect_response(secret))
+ Ok(connect_response_outcome(&connection, secret))
}
}
}
@@ -159,27 +191,30 @@ impl MycNip46Handler {
&self,
client_public_key: RadrootsNostrPublicKey,
request_message: RadrootsNostrConnectRequestMessage,
- ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ ) -> Result<MycNip46HandledRequest, MycError> {
let connection = match self.lookup_connection(client_public_key)? {
Ok(connection) => connection,
- Err(response) => return Ok(response),
+ Err(response) => return Ok(MycNip46HandledRequest::respond(response)),
};
let manager = self.signer.load_signer_manager()?;
let evaluation = manager.evaluate_request(&connection.connection_id, request_message)?;
match evaluation.action {
- RadrootsNostrSignerRequestAction::Denied { reason } => {
- Ok(RadrootsNostrConnectResponse::Error {
+ RadrootsNostrSignerRequestAction::Denied { reason } => Ok(
+ MycNip46HandledRequest::respond(RadrootsNostrConnectResponse::Error {
result: None,
error: reason,
- })
- }
- RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => Ok(
- RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
+ }),
),
+ RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => {
+ Ok(MycNip46HandledRequest::respond(
+ RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
+ ))
+ }
RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => {
response_from_hint(&evaluation.connection, response_hint)
+ .map(MycNip46HandledRequest::respond)
}
}
}
@@ -189,28 +224,30 @@ impl MycNip46Handler {
client_public_key: RadrootsNostrPublicKey,
request_message: RadrootsNostrConnectRequestMessage,
unsigned_event: nostr::UnsignedEvent,
- ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ ) -> Result<MycNip46HandledRequest, MycError> {
let connection = match self.lookup_connection(client_public_key)? {
Ok(connection) => connection,
- Err(response) => return Ok(response),
+ Err(response) => return Ok(MycNip46HandledRequest::respond(response)),
};
let manager = self.signer.load_signer_manager()?;
let evaluation = manager.evaluate_request(&connection.connection_id, request_message)?;
match evaluation.action {
- RadrootsNostrSignerRequestAction::Denied { reason } => {
- Ok(RadrootsNostrConnectResponse::Error {
+ RadrootsNostrSignerRequestAction::Denied { reason } => Ok(
+ MycNip46HandledRequest::respond(RadrootsNostrConnectResponse::Error {
result: None,
error: reason,
- })
- }
- RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => Ok(
- RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
+ }),
),
- RadrootsNostrSignerRequestAction::Allowed { .. } => {
- self.sign_event_response(unsigned_event)
+ RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => {
+ Ok(MycNip46HandledRequest::respond(
+ RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
+ ))
}
+ RadrootsNostrSignerRequestAction::Allowed { .. } => self
+ .sign_event_response(unsigned_event)
+ .map(MycNip46HandledRequest::respond),
}
}
@@ -218,27 +255,31 @@ impl MycNip46Handler {
&self,
client_public_key: RadrootsNostrPublicKey,
request_message: RadrootsNostrConnectRequestMessage,
- ) -> Result<RadrootsNostrConnectResponse, MycError> {
+ ) -> Result<MycNip46HandledRequest, MycError> {
let request = request_message.request.clone();
let connection = match self.lookup_connection(client_public_key)? {
Ok(connection) => connection,
- Err(response) => return Ok(response),
+ Err(response) => return Ok(MycNip46HandledRequest::respond(response)),
};
let manager = self.signer.load_signer_manager()?;
let evaluation = manager.evaluate_request(&connection.connection_id, request_message)?;
match evaluation.action {
- RadrootsNostrSignerRequestAction::Denied { reason } => {
- Ok(RadrootsNostrConnectResponse::Error {
+ RadrootsNostrSignerRequestAction::Denied { reason } => Ok(
+ MycNip46HandledRequest::respond(RadrootsNostrConnectResponse::Error {
result: None,
error: reason,
- })
- }
- RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => Ok(
- RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
+ }),
),
- RadrootsNostrSignerRequestAction::Allowed { .. } => self.crypto_response(request),
+ RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => {
+ Ok(MycNip46HandledRequest::respond(
+ RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
+ ))
+ }
+ RadrootsNostrSignerRequestAction::Allowed { .. } => self
+ .crypto_response(request)
+ .map(MycNip46HandledRequest::respond),
}
}
@@ -388,16 +429,25 @@ impl MycNip46Service {
};
let request_id = request_message.id.clone();
- let response = match self.handler.handle_request(event.pubkey, request_message) {
- Ok(response) => response,
+ let handled_request = match self.handler.handle_request(event.pubkey, request_message) {
+ Ok(handled_request) => handled_request,
Err(error) => {
tracing::warn!(error = %error, "failed to handle NIP-46 request");
- RadrootsNostrConnectResponse::Error {
+ MycNip46HandledRequest::respond(RadrootsNostrConnectResponse::Error {
result: None,
error: error.to_string(),
- }
+ })
}
};
+ let Some((response, consume_connect_secret_for)) = handled_request.into_publish_parts()
+ else {
+ tracing::debug!(
+ request_id = %request_id,
+ client_public_key = %event.pubkey,
+ "ignoring NIP-46 request without response"
+ );
+ continue;
+ };
let response_event =
self.handler
@@ -409,11 +459,50 @@ impl MycNip46Service {
.await
{
tracing::warn!(error = %error, "failed to publish NIP-46 response");
+ continue;
+ }
+ if let Some(connection_id) = consume_connect_secret_for {
+ if let Err(error) = self
+ .handler
+ .signer
+ .load_signer_manager()?
+ .mark_connect_secret_consumed(&connection_id)
+ {
+ tracing::warn!(
+ error = %error,
+ connection_id = %connection_id,
+ "failed to persist consumed NIP-46 connect secret"
+ );
+ }
}
}
}
}
+impl MycNip46HandledRequest {
+ fn respond(response: RadrootsNostrConnectResponse) -> Self {
+ Self::Respond {
+ response,
+ consume_connect_secret_for: None,
+ }
+ }
+
+ pub(crate) fn into_publish_parts(
+ self,
+ ) -> Option<(
+ RadrootsNostrConnectResponse,
+ Option<RadrootsNostrSignerConnectionId>,
+ )> {
+ match self {
+ Self::Respond {
+ response,
+ consume_connect_secret_for,
+ } => Some((response, consume_connect_secret_for)),
+ Self::Ignore => None,
+ }
+ }
+}
+
fn connect_response(secret: Option<String>) -> RadrootsNostrConnectResponse {
match secret {
Some(secret) => RadrootsNostrConnectResponse::ConnectSecretEcho(secret),
@@ -421,6 +510,17 @@ fn connect_response(secret: Option<String>) -> RadrootsNostrConnectResponse {
}
}
+fn connect_response_outcome(
+ connection: &RadrootsNostrSignerConnectionRecord,
+ secret: Option<String>,
+) -> MycNip46HandledRequest {
+ let consume_connect_secret_for = secret.as_ref().map(|_| connection.connection_id.clone());
+ MycNip46HandledRequest::Respond {
+ response: connect_response(secret),
+ consume_connect_secret_for,
+ }
+}
+
fn grant_permissions_for_new_connection(
requested_permissions: RadrootsNostrConnectPermissions,
) -> RadrootsNostrConnectPermissions {
@@ -471,7 +571,7 @@ mod tests {
use crate::app::MycRuntime;
use crate::config::{MycConfig, MycConnectionApproval};
- use super::MycNip46Handler;
+ use super::{MycNip46HandledRequest, MycNip46Handler};
fn write_identity(path: &std::path::Path, secret_key: &str) {
radroots_identity::RadrootsIdentity::from_secret_key_str(secret_key)
@@ -591,7 +691,7 @@ mod tests {
requested_permissions: Vec<RadrootsNostrConnectPermission>,
) {
handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-connect",
@@ -643,7 +743,7 @@ mod tests {
let runtime = runtime();
let handler = handler(&runtime);
let response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-connect",
@@ -674,12 +774,111 @@ mod tests {
}
#[test]
+ fn existing_unconsumed_connect_secret_can_still_retry_after_failed_publish() {
+ let runtime = runtime();
+ let handler = handler(&runtime);
+
+ let first = handler
+ .handle_request_response(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-connect-1",
+ RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: runtime.signer_identity().public_key(),
+ secret: Some("s3cr3t".to_owned()),
+ requested_permissions: Default::default(),
+ },
+ ),
+ )
+ .expect("first connect response");
+ let second = handler
+ .handle_request_response(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-connect-2",
+ RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: runtime.signer_identity().public_key(),
+ secret: Some("s3cr3t".to_owned()),
+ requested_permissions: Default::default(),
+ },
+ ),
+ )
+ .expect("second connect response");
+
+ assert_eq!(
+ first,
+ RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned())
+ );
+ assert_eq!(second, first);
+ }
+
+ #[test]
+ fn consumed_connect_secret_is_ignored_on_reuse() {
+ let runtime = runtime();
+ let handler = handler(&runtime);
+ let response = handler
+ .handle_request_response(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-connect",
+ RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: runtime.signer_identity().public_key(),
+ secret: Some("s3cr3t".to_owned()),
+ requested_permissions: Default::default(),
+ },
+ ),
+ )
+ .expect("connect response");
+ assert_eq!(
+ response,
+ RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned())
+ );
+
+ let connection = runtime
+ .signer_manager()
+ .expect("manager")
+ .list_connections()
+ .expect("connections")
+ .into_iter()
+ .next()
+ .expect("connection");
+ runtime
+ .signer_manager()
+ .expect("manager")
+ .mark_connect_secret_consumed(&connection.connection_id)
+ .expect("consume connect secret");
+
+ let ignored = handler
+ .handle_request(
+ client_keys().public_key(),
+ RadrootsNostrConnectRequestMessage::new(
+ "req-connect-reused",
+ RadrootsNostrConnectRequest::Connect {
+ remote_signer_public_key: runtime.signer_identity().public_key(),
+ secret: Some("s3cr3t".to_owned()),
+ requested_permissions: Default::default(),
+ },
+ ),
+ )
+ .expect("ignored response");
+
+ assert_eq!(ignored, MycNip46HandledRequest::Ignore);
+ let connections = runtime
+ .signer_manager()
+ .expect("manager")
+ .list_connections()
+ .expect("connections");
+ assert_eq!(connections.len(), 1);
+ assert!(connections[0].connect_secret_is_consumed());
+ }
+
+ #[test]
fn connect_preserves_pending_status_when_explicit_approval_is_required() {
let runtime = runtime_with_explicit_approval();
let handler = handler(&runtime);
let response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-connect",
@@ -717,7 +916,7 @@ mod tests {
let runtime = runtime();
let handler = handler(&runtime);
handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-connect",
@@ -734,7 +933,7 @@ mod tests {
.expect("connect");
let public_key = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-pubkey",
@@ -748,7 +947,7 @@ mod tests {
);
let pong = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-ping",
@@ -759,7 +958,7 @@ mod tests {
assert_eq!(pong, RadrootsNostrConnectResponse::Pong);
let relays = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-switch",
@@ -780,7 +979,7 @@ mod tests {
let runtime = runtime();
let handler = handler(&runtime);
handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-connect",
@@ -814,7 +1013,7 @@ mod tests {
connect_with_permissions(&handler, &runtime, vec![sign_event_permission(1)]);
let response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-sign",
@@ -843,7 +1042,7 @@ mod tests {
connect_with_permissions(&handler, &runtime, Vec::new());
let response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-sign",
@@ -872,7 +1071,7 @@ mod tests {
connect_with_permissions(&handler, &runtime, vec![sign_event_permission(1)]);
let response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-sign",
@@ -908,7 +1107,7 @@ mod tests {
);
let encrypt_response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-nip04-encrypt",
@@ -939,7 +1138,7 @@ mod tests {
)
.expect("client encrypt");
let decrypt_response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-nip04-decrypt",
@@ -970,7 +1169,7 @@ mod tests {
);
let encrypt_response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-nip44-encrypt",
@@ -1002,7 +1201,7 @@ mod tests {
)
.expect("client encrypt");
let decrypt_response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-nip44-decrypt",
@@ -1032,7 +1231,7 @@ mod tests {
);
let response = handler
- .handle_request(
+ .handle_request_response(
client_keys().public_key(),
RadrootsNostrConnectRequestMessage::new(
"req-nip04-decrypt",