@@ -428,6 +428,39 @@ impl BuzzClient {
428428 }
429429 }
430430
431+ /// Sign an event builder verbatim: no NIP-OA auth-tag injection, and none
432+ /// of [`sign_event`]'s "callers must not add auth tags" enforcement.
433+ ///
434+ /// Used only for NIP-IA identity archive/unarchive requests (kind
435+ /// 9035/9036), whose optional `auth` tag is a *content-level*
436+ /// owner-of-agent attestation about the *target* identity — unrelated to
437+ /// this client's own NIP-OA membership delegation (`self.auth_tag`,
438+ /// which [`sign_event`] injects into every other event and which
439+ /// `submit_event` separately attaches via the `x-auth-tag` HTTP header).
440+ /// Routing an identity archive request through `sign_event` would either
441+ /// silently drop the caller's owner attestation or double up an
442+ /// unrelated tag.
443+ pub fn sign_event_unchecked ( & self , builder : EventBuilder ) -> Result < nostr:: Event , CliError > {
444+ builder
445+ . sign_with_keys ( & self . keys )
446+ . map_err ( |e| CliError :: Other ( format ! ( "signing failed: {e}" ) ) )
447+ }
448+
449+ /// GET a public, unauthenticated relay endpoint (e.g. the NIP-11 `/info`
450+ /// document), returning the raw JSON body. No NIP-98 Authorization and no
451+ /// `x-auth-tag` header — the endpoint is public relay metadata, not a
452+ /// membership-scoped resource.
453+ pub async fn get_public ( & self , path : & str ) -> Result < String , CliError > {
454+ let url = format ! ( "{}{path}" , self . relay_url) ;
455+ let resp = self
456+ . http
457+ . get ( & url)
458+ . header ( "Accept" , "application/nostr+json" )
459+ . send ( )
460+ . await ?;
461+ self . handle_response ( resp) . await
462+ }
463+
431464 /// Execute a one-shot query via the HTTP bridge.
432465 /// `filter` is a Nostr filter object (will be wrapped in an array).
433466 /// Returns the raw JSON response (array of events).
@@ -849,7 +882,8 @@ pub fn normalize_write_response(raw: &str) -> String {
849882
850883#[ cfg( test) ]
851884mod tests {
852- use super :: { create_response_with_id, extract_relay_response_field} ;
885+ use super :: { create_response_with_id, extract_relay_response_field, BuzzClient } ;
886+ use nostr:: { EventBuilder , Keys , Kind , Tag } ;
853887
854888 #[ test]
855889 fn extract_relay_response_field_reads_response_message_json ( ) {
@@ -875,4 +909,125 @@ mod tests {
875909 assert_eq ! ( v[ "event_id" ] . as_str( ) , Some ( "abc" ) ) ;
876910 assert_eq ! ( v[ "accepted" ] . as_bool( ) , Some ( true ) ) ;
877911 }
912+
913+ // --- (a) auth-suppression regression pair ---
914+
915+ fn make_auth_tag ( ) -> ( Tag , String ) {
916+ let owner_hex = "a" . repeat ( 64 ) ;
917+ let sig_hex = "b" . repeat ( 128 ) ;
918+ let tag_vec = vec ! [
919+ "auth" . to_string( ) ,
920+ owner_hex,
921+ "conditions" . to_string( ) ,
922+ sig_hex,
923+ ] ;
924+ let json = serde_json:: to_string ( & tag_vec) . unwrap ( ) ;
925+ let tag = Tag :: parse ( tag_vec) . unwrap ( ) ;
926+ ( tag, json)
927+ }
928+
929+ #[ test]
930+ fn sign_event_unchecked_does_not_inject_ambient_auth_tag ( ) {
931+ let keys = Keys :: generate ( ) ;
932+ let ( auth_tag, auth_json) = make_auth_tag ( ) ;
933+ let client = BuzzClient :: new (
934+ "https://test.relay" . into ( ) ,
935+ keys,
936+ Some ( auth_tag) ,
937+ Some ( auth_json) ,
938+ )
939+ . unwrap ( ) ;
940+
941+ let builder =
942+ EventBuilder :: new ( Kind :: Custom ( 9035 ) , "archive" ) . tags ( [ Tag :: parse ( [ "-" ] ) . unwrap ( ) ] ) ;
943+ let event = client. sign_event_unchecked ( builder) . unwrap ( ) ;
944+
945+ let auth_tags: Vec < _ > = event
946+ . tags
947+ . iter ( )
948+ . filter ( |t| t. as_slice ( ) . first ( ) . map ( |s| s. as_str ( ) ) == Some ( "auth" ) )
949+ . collect ( ) ;
950+ assert ! (
951+ auth_tags. is_empty( ) ,
952+ "sign_event_unchecked must not inject the ambient NIP-OA auth tag \
953+ into identity archive events; found {auth_tags:?}"
954+ ) ;
955+ }
956+
957+ #[ test]
958+ fn sign_event_unchecked_preserves_callers_content_auth_tag ( ) {
959+ let keys = Keys :: generate ( ) ;
960+ let ( auth_tag, auth_json) = make_auth_tag ( ) ;
961+ let client = BuzzClient :: new (
962+ "https://test.relay" . into ( ) ,
963+ keys,
964+ Some ( auth_tag) ,
965+ Some ( auth_json) ,
966+ )
967+ . unwrap ( ) ;
968+
969+ let content_auth = Tag :: parse ( [
970+ "auth" ,
971+ & "c" . repeat ( 64 ) ,
972+ "owner-attestation" ,
973+ & "d" . repeat ( 128 ) ,
974+ ] )
975+ . unwrap ( ) ;
976+
977+ let builder = EventBuilder :: new ( Kind :: Custom ( 9035 ) , "archive" )
978+ . tags ( [ Tag :: parse ( [ "-" ] ) . unwrap ( ) , content_auth] ) ;
979+ let event = client. sign_event_unchecked ( builder) . unwrap ( ) ;
980+
981+ let auth_tags: Vec < _ > = event
982+ . tags
983+ . iter ( )
984+ . filter ( |t| t. as_slice ( ) . first ( ) . map ( |s| s. as_str ( ) ) == Some ( "auth" ) )
985+ . collect ( ) ;
986+ assert_eq ! (
987+ auth_tags. len( ) ,
988+ 1 ,
989+ "content-level auth tag must survive sign_event_unchecked; found {auth_tags:?}"
990+ ) ;
991+ assert_eq ! ( auth_tags[ 0 ] . as_slice( ) [ 1 ] , "c" . repeat( 64 ) ) ;
992+ }
993+
994+ #[ test]
995+ fn with_auth_tag_sets_header_when_configured ( ) {
996+ let keys = Keys :: generate ( ) ;
997+ let ( auth_tag, auth_json) = make_auth_tag ( ) ;
998+ let client = BuzzClient :: new (
999+ "https://test.relay" . into ( ) ,
1000+ keys,
1001+ Some ( auth_tag) ,
1002+ Some ( auth_json. clone ( ) ) ,
1003+ )
1004+ . unwrap ( ) ;
1005+
1006+ let req = client. http . post ( "https://test.relay/events" ) ;
1007+ let req = client. with_auth_tag ( req) ;
1008+ let built = req. build ( ) . unwrap ( ) ;
1009+ let header = built
1010+ . headers ( )
1011+ . get ( "x-auth-tag" )
1012+ . expect ( "x-auth-tag header must be present" ) ;
1013+ assert_eq ! (
1014+ header. to_str( ) . unwrap( ) ,
1015+ & auth_json,
1016+ "x-auth-tag header must carry the raw auth tag JSON"
1017+ ) ;
1018+ }
1019+
1020+ #[ test]
1021+ fn with_auth_tag_omits_header_when_not_configured ( ) {
1022+ let keys = Keys :: generate ( ) ;
1023+ let client = BuzzClient :: new ( "https://test.relay" . into ( ) , keys, None , None ) . unwrap ( ) ;
1024+
1025+ let req = client. http . post ( "https://test.relay/events" ) ;
1026+ let req = client. with_auth_tag ( req) ;
1027+ let built = req. build ( ) . unwrap ( ) ;
1028+ assert ! (
1029+ built. headers( ) . get( "x-auth-tag" ) . is_none( ) ,
1030+ "x-auth-tag header must not be present when no auth tag is configured"
1031+ ) ;
1032+ }
8781033}
0 commit comments