mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
refactor(desktop): share relay request construction (#7667)
## Summary This is a behavior-preserving refactor that gives the desktop relay HTTP bridge one helper for authenticated request construction. It moves the repeated `Authorization`, JSON content type, optional `x-auth-tag`, body, and timeout setup into `build_authenticated_relay_request`. The helper uses the final URL, auth header, and body bytes passed by the caller. It does not sign, reserialize, normalize, send, parse, classify errors, choose a client, or handle rate limits. Excluded paths: media, WebSocket, Git, agents, mobile, and NIP-FI. The query send helper only routes through the new request builder so the timeout and `x-auth-tag` behavior are covered by the same seam. ### Related issue None found. This prepares for the enterprise identity follow-up to be smaller than #7663. ### Testing - `cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check` passed at `356676a2f70f6bdf80cfdcb9084f1f7548359ecb`. - `cargo test --manifest-path desktop/src-tauri/Cargo.toml relay::tests::authenticated -- --nocapture` passed at `356676a2f70f6bdf80cfdcb9084f1f7548359ecb`. - `just desktop-tauri-check` passed at `356676a2f70f6bdf80cfdcb9084f1f7548359ecb`. - `just desktop-tauri-test` passed at `356676a2f70f6bdf80cfdcb9084f1f7548359ecb`. - `just desktop-tauri-clippy` passed at `356676a2f70f6bdf80cfdcb9084f1f7548359ecb`. Generated with Codex Signed-off-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@buzz.block.builderlab.xyz> Co-authored-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use nostr::{EventBuilder, JsonUtil, Keys, Kind, Tag};
|
||||
use reqwest::Method;
|
||||
use reqwest::{Method, RequestBuilder};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -409,6 +409,37 @@ pub async fn query_relay_at_with_keys(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Build an authenticated relay HTTP request using already-signed NIP-98 auth.
|
||||
///
|
||||
/// The caller owns request ordering around this helper: rate-limit admission,
|
||||
/// egress checks, URL/body construction, send, error classification, and response
|
||||
/// parsing remain outside. `body` is accepted as final bytes so this helper never
|
||||
/// reserializes, normalizes, or changes the payload that was signed.
|
||||
fn build_authenticated_relay_request(
|
||||
client: &reqwest::Client,
|
||||
method: Method,
|
||||
url: &str,
|
||||
auth: &str,
|
||||
body: Option<Vec<u8>>,
|
||||
auth_tag: Option<&str>,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> RequestBuilder {
|
||||
let mut request = client.request(method, url).header("Authorization", auth);
|
||||
if body.is_some() {
|
||||
request = request.header("Content-Type", "application/json");
|
||||
}
|
||||
if let Some(tag) = auth_tag {
|
||||
request = request.header("x-auth-tag", tag);
|
||||
}
|
||||
if let Some(timeout) = timeout {
|
||||
request = request.timeout(timeout);
|
||||
}
|
||||
if let Some(body) = body {
|
||||
request = request.body(body);
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
/// Issue an authenticated `POST /query` and parse the response, applying the
|
||||
/// per-request `timeout` that bounds a stalled or half-open relay connection.
|
||||
///
|
||||
@@ -425,19 +456,18 @@ async fn send_query_request(
|
||||
body_bytes: Vec<u8>,
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<Vec<nostr::Event>, String> {
|
||||
let mut request = http_client
|
||||
.post(url)
|
||||
.header("Authorization", auth)
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(timeout);
|
||||
if let Some(tag) = auth_tag {
|
||||
request = request.header("x-auth-tag", tag);
|
||||
}
|
||||
let response = request
|
||||
.body(body_bytes)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| classify_request_error(&e))?;
|
||||
let response = build_authenticated_relay_request(
|
||||
http_client,
|
||||
Method::POST,
|
||||
url,
|
||||
auth,
|
||||
Some(body_bytes),
|
||||
auth_tag,
|
||||
Some(timeout),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| classify_request_error(&e))?;
|
||||
if !response.status().is_success() {
|
||||
return Err(relay_error_message(response).await);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use serde::de::DeserializeOwned;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
use super::{
|
||||
build_nip98_auth_header, classify_request_error, parse_json_response,
|
||||
relay_api_base_url_with_override, relay_error_message,
|
||||
build_authenticated_relay_request, build_nip98_auth_header, classify_request_error,
|
||||
parse_json_response, relay_api_base_url_with_override, relay_error_message,
|
||||
};
|
||||
|
||||
/// Execute an authenticated GET against the active relay and decode its JSON body.
|
||||
@@ -23,13 +23,18 @@ pub async fn get_relay_json<T: DeserializeOwned>(
|
||||
path_with_query
|
||||
);
|
||||
let auth = build_nip98_auth_header(&Method::GET, &url, &[], state)?;
|
||||
let response = state
|
||||
.http_client
|
||||
.get(&url)
|
||||
.header("Authorization", auth)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| classify_request_error(&error))?;
|
||||
let response = build_authenticated_relay_request(
|
||||
&state.http_client,
|
||||
Method::GET,
|
||||
&url,
|
||||
&auth,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| classify_request_error(&error))?;
|
||||
if !response.status().is_success() {
|
||||
return Err(relay_error_message(response).await);
|
||||
}
|
||||
|
||||
@@ -28,15 +28,18 @@ pub async fn submit_signed_event_at_with_keys(
|
||||
crate::egress_guard::assert_no_key_backup_bytes(&body_bytes, "relay event submit")?;
|
||||
let auth_header = build_nip98_auth_header_for_keys(keys, &Method::POST, &url, &body_bytes)?;
|
||||
|
||||
let response = state
|
||||
.http_client
|
||||
.post(&url)
|
||||
.header("Authorization", auth_header)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body_bytes)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| classify_request_error(&e))?;
|
||||
let response = build_authenticated_relay_request(
|
||||
&state.http_client,
|
||||
Method::POST,
|
||||
&url,
|
||||
&auth_header,
|
||||
Some(body_bytes),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| classify_request_error(&e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(relay_error_message(response).await);
|
||||
|
||||
@@ -2,12 +2,162 @@
|
||||
//! Extracted from `relay.rs` to keep that module under the file-size ratchet.
|
||||
|
||||
use super::{
|
||||
build_profile_event, classify_intercepted_response, effective_agent_relay_url,
|
||||
extract_retry_in_hint, parse_command_response, relay_http_base_url, MALFORMED_RESPONSE_MESSAGE,
|
||||
build_authenticated_relay_request, build_profile_event, classify_intercepted_response,
|
||||
effective_agent_relay_url, extract_retry_in_hint, parse_command_response, relay_http_base_url,
|
||||
MALFORMED_RESPONSE_MESSAGE,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
// ── extract_retry_in_hint ────────────────────────────────────────────────
|
||||
const AUTH_HEADER: reqwest::header::HeaderName = reqwest::header::AUTHORIZATION;
|
||||
const CONTENT_TYPE_HEADER: reqwest::header::HeaderName = reqwest::header::CONTENT_TYPE;
|
||||
const X_AUTH_TAG_HEADER: reqwest::header::HeaderName =
|
||||
reqwest::header::HeaderName::from_static("x-auth-tag");
|
||||
|
||||
fn assert_header_values(
|
||||
request: &reqwest::Request,
|
||||
expected: &[(&reqwest::header::HeaderName, &[&str])],
|
||||
) {
|
||||
let headers = request.headers();
|
||||
let actual_names: std::collections::BTreeSet<String> = headers
|
||||
.keys()
|
||||
.map(|name| name.as_str().to_ascii_lowercase())
|
||||
.collect();
|
||||
let expected_names: std::collections::BTreeSet<String> = expected
|
||||
.iter()
|
||||
.map(|(name, _)| name.as_str().to_ascii_lowercase())
|
||||
.collect();
|
||||
assert_eq!(actual_names, expected_names, "unexpected header set");
|
||||
|
||||
for (name, values) in expected {
|
||||
let actual: Vec<&str> = headers
|
||||
.get_all(*name)
|
||||
.iter()
|
||||
.map(|value| value.to_str().expect("test header must be visible text"))
|
||||
.collect();
|
||||
assert_eq!(actual, *values, "unexpected values for {name}");
|
||||
}
|
||||
}
|
||||
|
||||
fn request_body_bytes(request: &reqwest::Request) -> Option<&[u8]> {
|
||||
request.body().and_then(reqwest::Body::as_bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticated_get_request_matches_existing_shape() {
|
||||
let client = reqwest::Client::new();
|
||||
let request = build_authenticated_relay_request(
|
||||
&client,
|
||||
reqwest::Method::GET,
|
||||
"https://relay.example/info?verbose=1",
|
||||
"Nostr signed-get",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.build()
|
||||
.expect("request should build");
|
||||
|
||||
assert_eq!(request.method(), reqwest::Method::GET);
|
||||
assert_eq!(
|
||||
request.url().as_str(),
|
||||
"https://relay.example/info?verbose=1"
|
||||
);
|
||||
assert_header_values(&request, &[(&AUTH_HEADER, &["Nostr signed-get"])]);
|
||||
assert!(request.timeout().is_none());
|
||||
assert!(request_body_bytes(&request).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticated_json_post_request_preserves_final_body_bytes() {
|
||||
let client = reqwest::Client::new();
|
||||
let body = br#"[{"kinds":[9],"limit":1}]"#.to_vec();
|
||||
let request = build_authenticated_relay_request(
|
||||
&client,
|
||||
reqwest::Method::POST,
|
||||
"https://relay.example/query",
|
||||
"Nostr signed-post",
|
||||
Some(body.clone()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.build()
|
||||
.expect("request should build");
|
||||
|
||||
assert_eq!(request.method(), reqwest::Method::POST);
|
||||
assert_eq!(request.url().as_str(), "https://relay.example/query");
|
||||
assert_header_values(
|
||||
&request,
|
||||
&[
|
||||
(&AUTH_HEADER, &["Nostr signed-post"]),
|
||||
(&CONTENT_TYPE_HEADER, &["application/json"]),
|
||||
],
|
||||
);
|
||||
assert!(
|
||||
request.headers().get("x-auth-tag").is_none(),
|
||||
"absence of x-auth-tag must remain absence, not an empty header"
|
||||
);
|
||||
assert!(request.timeout().is_none());
|
||||
assert_eq!(request_body_bytes(&request), Some(body.as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticated_json_post_request_keeps_auth_tag_when_present() {
|
||||
let client = reqwest::Client::new();
|
||||
let body = b"{}".to_vec();
|
||||
let request = build_authenticated_relay_request(
|
||||
&client,
|
||||
reqwest::Method::POST,
|
||||
"https://relay.example/event-submit",
|
||||
"Nostr signed-event",
|
||||
Some(body.clone()),
|
||||
Some("owner-auth-tag"),
|
||||
None,
|
||||
)
|
||||
.build()
|
||||
.expect("request should build");
|
||||
|
||||
assert_eq!(request.method(), reqwest::Method::POST);
|
||||
assert_eq!(request.url().as_str(), "https://relay.example/event-submit");
|
||||
assert_header_values(
|
||||
&request,
|
||||
&[
|
||||
(&AUTH_HEADER, &["Nostr signed-event"]),
|
||||
(&CONTENT_TYPE_HEADER, &["application/json"]),
|
||||
(&X_AUTH_TAG_HEADER, &["owner-auth-tag"]),
|
||||
],
|
||||
);
|
||||
assert!(request.timeout().is_none());
|
||||
assert_eq!(request_body_bytes(&request), Some(body.as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticated_query_request_preserves_timeout() {
|
||||
let client = reqwest::Client::new();
|
||||
let timeout = std::time::Duration::from_millis(250);
|
||||
let request = build_authenticated_relay_request(
|
||||
&client,
|
||||
reqwest::Method::POST,
|
||||
"https://relay.example/query",
|
||||
"Nostr signed-query",
|
||||
Some(b"[]".to_vec()),
|
||||
None,
|
||||
Some(timeout),
|
||||
)
|
||||
.build()
|
||||
.expect("request should build");
|
||||
|
||||
assert_eq!(request.method(), reqwest::Method::POST);
|
||||
assert_eq!(request.url().as_str(), "https://relay.example/query");
|
||||
assert_header_values(
|
||||
&request,
|
||||
&[
|
||||
(&AUTH_HEADER, &["Nostr signed-query"]),
|
||||
(&CONTENT_TYPE_HEADER, &["application/json"]),
|
||||
],
|
||||
);
|
||||
assert_eq!(request.timeout(), Some(&timeout));
|
||||
assert_eq!(request_body_bytes(&request), Some(&b"[]"[..]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_hint_from_429_body() {
|
||||
|
||||
Reference in New Issue
Block a user