@@ -146,6 +146,26 @@ pub(crate) fn verify_bridge_auth_with_options(
146146 Err ( api_error ( StatusCode :: UNAUTHORIZED , "missing Nostr auth" ) )
147147}
148148
149+ /// A declared dev-mode identity is not proof of authorship. Preserve the
150+ /// public-kind fallback, but require verified NIP-98 for any filter that could
151+ /// read author-only data, including mixed-kind and known-ID queries.
152+ fn authorize_author_only_read (
153+ filters : & [ nostr:: Filter ] ,
154+ signed_auth_created_at : Option < u64 > ,
155+ ) -> Result < ( ) , ( StatusCode , Json < Value > ) > {
156+ if signed_auth_created_at. is_none ( )
157+ && filters
158+ . iter ( )
159+ . any ( crate :: handlers:: req:: filter_can_match_author_only_kinds)
160+ {
161+ return Err ( api_error (
162+ StatusCode :: UNAUTHORIZED ,
163+ "auth-required: author-only reads require NIP-98 authentication" ,
164+ ) ) ;
165+ }
166+ Ok ( ( ) )
167+ }
168+
149169/// Check NIP-98 replay and record the event ID atomically.
150170///
151171/// The correctness boundary is the shared, community-scoped Redis seen-set on
@@ -1121,6 +1141,8 @@ async fn query_events_authed(
11211141 crate :: handlers:: req:: extract_channel_ids_from_filters_limited ( & filters)
11221142 . map_err ( |( ) | api_error ( StatusCode :: BAD_REQUEST , "too many explicit channels" ) ) ?;
11231143
1144+ authorize_author_only_read ( & filters, signed_auth_created_at) ?;
1145+
11241146 // P-gated kinds (gift wraps, member notifications, observer frames) require
11251147 // the caller's own pubkey in the #p tag — same enforcement as WS REQ handler.
11261148 let authed_pubkey_hex = pubkey. to_hex ( ) ;
@@ -1655,6 +1677,8 @@ async fn count_events_authed(
16551677 crate :: handlers:: req:: extract_channel_ids_from_filters_limited ( & filters)
16561678 . map_err ( |( ) | api_error ( StatusCode :: BAD_REQUEST , "too many explicit channels" ) ) ?;
16571679
1680+ authorize_author_only_read ( & filters, signed_auth_created_at) ?;
1681+
16581682 // P-gated kinds enforcement — same as WS REQ and /query.
16591683 let authed_pubkey_hex = pubkey. to_hex ( ) ;
16601684 if !crate :: handlers:: req:: p_gated_filters_authorized ( & filters, & authed_pubkey_hex) {
@@ -2529,6 +2553,10 @@ fn ban_json(b: &buzz_db::moderation::BanRecord) -> Value {
25292553 } )
25302554}
25312555
2556+ #[ cfg( test) ]
2557+ #[ path = "private_read_postgres_tests.rs" ]
2558+ mod private_read_postgres_tests;
2559+
25322560#[ cfg( test) ]
25332561mod postgres_tests {
25342562 use super :: * ;
@@ -3839,7 +3867,7 @@ mod postgres_tests {
38393867 /// - Redis pool points at the local dev instance for the admission check.
38403868 ///
38413869 /// Returns `None` when local Postgres is not reachable.
3842- async fn bridge_handler_test_state ( ) -> Option < Arc < crate :: state:: AppState > > {
3870+ pub ( super ) async fn bridge_handler_test_state ( ) -> Option < Arc < crate :: state:: AppState > > {
38433871 let mut config = crate :: config:: Config :: from_env ( ) . ok ( ) ?;
38443872 config. database_url = crate :: test_support:: database_url ( ) ;
38453873 // Use the real local Redis so enforce_http_admission can pass.
0 commit comments