Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e56c80d
feat(desktop): add in-app admin console for relay operators
Aug 4, 2026
19227ee
fix(admin): address pass-1 review findings on desktop admin surface
Aug 4, 2026
980281c
fix(admin): address pass-2 review findings on desktop admin surface
Aug 4, 2026
00128ec
fix(admin): address pass-3 review findings on desktop admin surface
Aug 4, 2026
a47fafe
test(admin): complete round-3 acceptance criteria
Aug 5, 2026
b9248b9
test(admin): switch to jsdom+RTL for event-driven tests; gate NIP-98 …
Aug 5, 2026
c626969
test(admin): isolate loadGenRef mutation via back-navigation; harden …
Aug 5, 2026
ec084d3
test(admin): full-typed DTO, pinned rejection fixtures, logout and cr…
Aug 5, 2026
246f0e4
test(admin): authorized→logout transition test; cross-identity no-pro…
Aug 5, 2026
284c391
fix(admin-console): replace isMountedRef with StrictMode-safe session…
Aug 5, 2026
eecc7a4
fix(settings): add admin-console to settings sidebar nav
Aug 6, 2026
9bb1666
chore: merge origin/main — resolve mod.rs conflict (keep pub mod admi…
Aug 6, 2026
7e633d7
fix(admin-console): structured detail views, disabled-mode panel gate…
Aug 6, 2026
f1fe900
refactor(desktop): extract PTT plugin builder to ptt_shortcut::build_…
Aug 6, 2026
a5ff7a9
Merge remote-tracking branch 'origin/main' into hayt/librs-trim
Aug 6, 2026
ea3e8a0
fix(admin-console): wire exact DTO types, fix detail field layouts, r…
Aug 6, 2026
535ee89
fix(admin-console): type feedback list as FeedbackSummary, pin contra…
Aug 7, 2026
34dfc86
fix(desktop): make terminal output selectable (#4980)
wesbillman Aug 7, 2026
e7da954
fix(desktop): preserve authoritative agent avatars (#4984)
tellaho Aug 7, 2026
b530b61
feat(admin-console): Phase 3 — role/source badges, action matrix, enf…
Aug 7, 2026
fa29ae1
fix(desktop): remove conflict marker from CHANGELOG, revert docs/admi…
Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
# Changelog

## Unreleased

### Desktop and shared changes

- feat(desktop): add in-app admin console for relay operators — NIP-98 client for deployment-wide reports and product feedback (`Settings → Admin console`)

## v0.5.5

### Desktop and shared changes

- feat: paste composer text without formatting ([#4801](https://github.com/block/buzz/pull/4801)) ([`25a9cf1be6d245fbd7373cb1160dbc790baf5bd5`](https://github.com/block/buzz/commit/25a9cf1be6d245fbd7373cb1160dbc790baf5bd5))
- Revert "chore(release): release Buzz Desktop version 0.5.5" ([#4808](https://github.com/block/buzz/pull/4808)) ([`79c52166cfe6b6d36bdc7686f943595c74e2f578`](https://github.com/block/buzz/commit/79c52166cfe6b6d36bdc7686f943595c74e2f578))
- chore(release): release Buzz Desktop version 0.5.5 ([#4800](https://github.com/block/buzz/pull/4800)) ([`a0ed13de14ee64dd90c32335790f7d3b4e94330d`](https://github.com/block/buzz/commit/a0ed13de14ee64dd90c32335790f7d3b4e94330d))
- fix: reauthenticate databricks model discovery ([#4008](https://github.com/block/buzz/pull/4008)) ([`4a2305170eef565bf1836e2859247e67c030f8af`](https://github.com/block/buzz/commit/4a2305170eef565bf1836e2859247e67c030f8af))
- Revert "chore(release): release Buzz Desktop version 0.5.5" ([#4797](https://github.com/block/buzz/pull/4797)) ([`8faf09f9aedb4989e57c7b6c5bd1052a444a3370`](https://github.com/block/buzz/commit/8faf09f9aedb4989e57c7b6c5bd1052a444a3370))
- feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues ([#4695](https://github.com/block/buzz/pull/4695)) ([`a1d78f2959b41c63f063ff818076d38c31071a47`](https://github.com/block/buzz/commit/a1d78f2959b41c63f063ff818076d38c31071a47))
- fix(desktop): serialize tray channel actions for frontend ([#4762](https://github.com/block/buzz/pull/4762)) ([`4c665aeac366fca5097eaa1088fb87f3d248eac7`](https://github.com/block/buzz/commit/4c665aeac366fca5097eaa1088fb87f3d248eac7))
- chore(release): release Buzz Desktop version 0.5.5 ([#4788](https://github.com/block/buzz/pull/4788)) ([`b948c54792c4933b4e003d2b227dc6e1f7c05fb4`](https://github.com/block/buzz/commit/b948c54792c4933b4e003d2b227dc6e1f7c05fb4))
- feat(projects): support multiple repositories ([#4671](https://github.com/block/buzz/pull/4671)) ([`e30db7028f9f1dc7646b5814ed03b4c54a4d2a48`](https://github.com/block/buzz/commit/e30db7028f9f1dc7646b5814ed03b4c54a4d2a48))
- fix(desktop): widen post-Enter timeouts in empty-edit-delete spec ([#4792](https://github.com/block/buzz/pull/4792)) ([`7bcfe7e0a141900d6e1e5bd0b3bce488b57d6453`](https://github.com/block/buzz/commit/7bcfe7e0a141900d6e1e5bd0b3bce488b57d6453))
- fix(desktop): wait for terminal frame before splash ([#4781](https://github.com/block/buzz/pull/4781)) ([`65f7a100353b9a5302da2614f2d85edee1c136a2`](https://github.com/block/buzz/commit/65f7a100353b9a5302da2614f2d85edee1c136a2))
- fix(desktop): integer-align custom reaction emoji ([#4779](https://github.com/block/buzz/pull/4779)) ([`8b8d86c5d26e2fa8cf419fdd8d0e56433f95d71a`](https://github.com/block/buzz/commit/8b8d86c5d26e2fa8cf419fdd8d0e56433f95d71a))
Expand Down
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"lint": "biome lint .",
"check": "biome check . && pnpm check:file-sizes && pnpm check:px-text && pnpm check:pubkey-truncation",
"format": "biome format --write .",
"test": "node --import ./test-loader.mjs --experimental-strip-types --test \"src/**/*.test.mjs\"",
"test": "node --import ./test-loader.mjs --experimental-strip-types --test \"src/**/*.test.mjs\" && node --import ./test-jsdom-setup.mjs --import ./test-loader.mjs --experimental-strip-types --test-force-exit --test \"src/**/*.jsdom-test.mjs\"",
"preview": "vite preview",
"tauri": "tauri",
"test:e2e": "pnpm build:e2e && playwright test",
Expand Down
2 changes: 2 additions & 0 deletions desktop/scripts/check-pubkey-truncation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const overrides = new Set([
"src/features/messages/lib/threadPanel.ts:395",
"src/features/projects/ui/ProjectsView.tsx:166",
"src/features/projects/ui/ProjectsOverviewPanel.tsx:209",
// Error message prefix in a console-internal action error (never rendered as identity).
"src/features/admin-console/AdminConsoleStaffingTab.tsx:108",
]);

await runPubkeyTruncationCheck({
Expand Down
7 changes: 7 additions & 0 deletions desktop/src-tauri/crates/buzz-terminal/src/damage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ pub struct Style {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RowFrame {
pub line: usize,
/// Whether this row continues onto the next screen row without a hard
/// line break. Retained separately from visual style so copy serialization
/// can reconstruct logical lines without exposing geometry flags to spans.
pub wrapped: bool,
pub spans: Vec<Span>,
}

Expand Down Expand Up @@ -332,6 +336,9 @@ impl Encoder {
self.hashes[line] = hash;
rows.push(RowFrame {
line,
wrapped: cells
.last()
.is_some_and(|cell| cell.flags.contains(Flags::WRAPLINE)),
spans: spans(&cells),
});
}
Expand Down
4 changes: 4 additions & 0 deletions desktop/src-tauri/crates/buzz-terminal/tests/clusters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ fn wrapping_does_not_split_a_uniform_run() {
.iter()
.find(|row| row.line == 0)
.expect("wrapped row must be present");
assert!(
first.wrapped,
"soft-wrap geometry must survive row encoding"
);
let texts: Vec<&str> = first.spans.iter().map(|s| s.text.as_str()).collect();
assert_eq!(
texts,
Expand Down
100 changes: 100 additions & 0 deletions desktop/src-tauri/src/commands/admin/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Dedicated no-redirect HTTP client for admin API requests.
//!
//! A separate client (not the app-wide `http_client`) ensures that:
//! - 3xx responses are surfaced as errors rather than followed — preventing
//! redirect-hop SSRF where a relay-issued redirect could forward the NIP-98
//! `Authorization` header to an off-origin host.
//! - Timeouts are tuned for synchronous UI feedback rather than media downloads.

use std::sync::OnceLock;

/// Request timeout for admin API calls.
pub(crate) const ADMIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);

/// The module-level singleton admin HTTP client.
///
/// Built once via `OnceLock` — panics on build failure so there is no
/// silent fallback to a redirect-following client.
pub static ADMIN_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();

/// Initialise the admin client singleton. Must be called from `setup()` before
/// any admin command can be invoked. Subsequent calls are no-ops.
pub fn init_admin_client() {
ADMIN_CLIENT.get_or_init(|| {
reqwest::Client::builder()
.resolve("localhost", std::net::SocketAddr::from(([127, 0, 0, 1], 0)))
.pool_idle_timeout(std::time::Duration::from_secs(10))
.pool_max_idle_per_host(2)
.redirect(reqwest::redirect::Policy::none())
.timeout(ADMIN_TIMEOUT)
.build()
.expect(
"admin HTTP client must build with redirect::Policy::none(); \
a redirect-following fallback would forward the NIP-98 \
Authorization header across origins (redirect-hop SSRF)",
)
});
}

#[cfg(test)]
mod tests {
use super::*;

/// The admin client must be buildable and must refuse to follow redirects.
/// This mirrors the `build_media_fetch_client_succeeds_with_no_redirect_policy`
/// test in `media_download.rs`.
#[test]
fn admin_client_builds_with_no_redirect_policy() {
init_admin_client();
assert!(ADMIN_CLIENT.get().is_some());
}

/// A live test that the client does not follow a 302.
///
/// Mirrors `media_fetch_client_does_not_follow_redirects` in
/// `media_download.rs`. Serves a 302 pointing at the metadata endpoint
/// and asserts exactly one connection was accepted.
#[tokio::test]
async fn admin_client_does_not_follow_redirects() {
use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

init_admin_client();
let client = ADMIN_CLIENT.get().expect("client initialised");

let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let connections = Arc::new(AtomicUsize::new(0));

let server_connections = Arc::clone(&connections);
let server = std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
server_connections.fetch_add(1, Ordering::SeqCst);
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf);
let response = "HTTP/1.1 302 Found\r\n\
Location: http://169.254.169.254/latest/meta-data/\r\n\
Content-Length: 0\r\n\
Connection: close\r\n\r\n";
let _ = stream.write_all(response.as_bytes());
let _ = stream.flush();
}
});

let resp = client
.get(format!("http://{addr}/api/admin/v1/reports"))
.timeout(std::time::Duration::from_secs(5))
.send()
.await
.expect("request should complete without following the redirect");

assert_eq!(resp.status().as_u16(), 302);
server.join().unwrap();
assert_eq!(
connections.load(Ordering::SeqCst),
1,
"exactly one request must be issued — redirect must not be followed",
);
}
}
Loading