Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
486aca7
feat(relay): OPERATOR/MODERATOR role model for relay admin API with N…
Aug 10, 2026
20d065f
feat(relay): advertise admin API origin via NIP-11 admin_api field
Aug 11, 2026
6f55ac0
fix(relay): stop requiring operator API origin for admin console boot
Aug 11, 2026
666da1e
fix(relay): reject unbracketed IPv6 in BUZZ_ADMIN_HOST at config parse
Aug 11, 2026
b2fb224
fix(relay): reject any unparseable BUZZ_ADMIN_HOST authority at confi…
Aug 11, 2026
91042b9
fix(relay): require BUZZ_ADMIN_HOST to be a bare authority, not just …
Aug 11, 2026
37d20b1
feat(relay): add reopen/cancel admin endpoints and activeAction repor…
Aug 13, 2026
6abc670
fix(relay): make admin cancel atomic + ownership-fenced (cross-report…
Aug 13, 2026
584941b
fix(relay): derive event-report enforcement target from stored author
Aug 13, 2026
ac88b9b
fix(relay): sync schema.sql admin tables to migrations 0034/0035
Aug 13, 2026
05ca744
fix(relay): align outbox pending index with pgschema-representable shape
Aug 13, 2026
6157e92
fix(relay): claim NIP-98 replay slot only after authorization, attrib…
Aug 14, 2026
ac4917f
feat(relay): make admin token mode read-write via relay identity
Aug 19, 2026
cb746de
test(relay): pin token-mode read-write attribution in CI
Aug 19, 2026
39e102b
test(relay): make token-mode dismiss gate hermetic
Aug 19, 2026
53a0d49
fix(admin-auth): harden NIP-98 and Host/Origin security surface
wpfleger96 Aug 20, 2026
14f0215
ci(relay): run admin API auth-boundary tests in the unit job
Aug 20, 2026
104aa20
Merge remote-tracking branch 'origin/main' into wpfleger/admin-api-be…
Aug 20, 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
53 changes: 53 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,59 @@ RELAY_URL=ws://localhost:3000
# (use `just web` for Vite HMR instead).
# BUZZ_WEB_DIR=./web/dist

# -----------------------------------------------------------------------------
# Admin Dashboard (private moderation surface)
# -----------------------------------------------------------------------------
# Host name that serves the read-only moderation dashboard and its
# /api/admin/v1 endpoints. Leave unset to keep the admin surface absent.
# Setting it requires one of the two authentication variables below.
# BUZZ_ADMIN_HOST=admin.localhost:3000
#
# Authentication mode. Accepted values: token (default), disabled, nip98.
# Any other value is a startup error.
# BUZZ_ADMIN_AUTH=token
#
# Option A — BUZZ_ADMIN_AUTH=token (default when unset):
# Exactly 64 hex characters (32 bytes); generate with `openssl rand -hex 32`.
# `just admin` generates a throwaway one per run and prints it.
# BUZZ_ADMIN_TOKEN=<64 hex characters>
# Token mode is read-only unless the relay has a stable identity: with a
# configured BUZZ_RELAY_PRIVATE_KEY (or the deterministic dev key when
# BUZZ_REQUIRE_AUTH_TOKEN=false), token-holder mutations and staffing are enabled
# and attributed to the relay's own pubkey as Operator — the honest actor for a
# shared deployment secret. Per-person attribution degrades to per-deployment —
# one shared secret is one actor identity; teams needing per-person attribution
# should use nip98.
#
# Option B — BUZZ_ADMIN_AUTH=disabled (network-layer auth only):
# Set only when the admin API is already protected at the network layer
# (VPN, private ingress). The relay logs a WARN on every startup.
# BUZZ_ADMIN_TOKEN must NOT also be set.
#
# Option C — BUZZ_ADMIN_AUTH=nip98 (Nostr pubkey-based auth):
# NIP-98 HTTP Auth. Each request must carry an Authorization: Nostr header
# with a signed kind-27235 event. Authorized principals are resolved from:
# 1. RELAY_OPERATOR_PUBKEYS — comma-separated 64-char hex pubkeys (config Operators).
# 2. RELAY_OWNER_PUBKEY — implicit Operator fallback when RELAY_OPERATOR_PUBKEYS is unset.
# 3. relay_operators table — DB-managed Operator/Moderator roster.
# The dashboard requires a NIP-07 browser extension.
# Setting RELAY_OPERATOR_PUBKEYS for the admin console does NOT require
# RELAY_OPERATOR_API_ORIGIN; that origin is only for community provisioning
# (see below). When BUZZ_ADMIN_HOST is set, the relay advertises the admin
# origin in its NIP-11 document (`admin_api` field) so clients can auto-discover
# the console without manual URL entry.
# RELAY_OPERATOR_PUBKEYS=<64-char hex pubkey>[,<64-char hex pubkey>...]
#
# Directory holding the built dashboard assets (`pnpm -C admin-web build`).
# BUZZ_ADMIN_WEB_DIR=./admin-web/dist
#
# Canonical origin (http(s)://host[:port], no path) that community-provisioning
# NIP-98 requests are verified against. Required only to USE the provisioning
# endpoints (POST /operator/communities) — not for the admin console. When
# RELAY_OPERATOR_PUBKEYS is set but this is unset, the relay boots with a WARN
# and provisioning requests fail closed until it is set.
# RELAY_OPERATOR_API_ORIGIN=http://127.0.0.1:3000

# Shared Redis-backed admission limits. Defaults shown below; each value must
# be a positive integer.
# BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=60
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,19 @@ jobs:
--run-ignored ignored-only
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Admin API token-mode read-write tests
# The only real HTTP → relay-token principal → mutation → cross-table
# attribution coverage: token-mode dismiss attributes to the relay key
# with relay_operator authority, and token-mode staffing PUT/DELETE
# records the relay key as added_by. #[ignore]d in the default suite —
# see api::admin::tests::token_mode_with_relay_identity_*.
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-relay) and test(/api::admin::tests::token_mode_with_relay_identity/)' \
--run-ignored ignored-only
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Upload relay log
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand Down
44 changes: 43 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
# Changelog

## Unreleased

- **Breaking:** the relay admin moderation API (`/api/admin/v1`) now requires
explicit authentication configuration when `BUZZ_ADMIN_HOST` is set. Choose
one mode via `BUZZ_ADMIN_AUTH` (unset defaults to `token`):
- **`BUZZ_ADMIN_AUTH=token` (default):** set `BUZZ_ADMIN_TOKEN` to exactly
64 hex characters (`openssl rand -hex 32`). Every request requires
`Authorization: Bearer`. The dashboard prompts for the token on first load.
- **`BUZZ_ADMIN_AUTH=disabled`:** admin API is unauthenticated. Use only when
the admin API is already protected by a VPN or private ingress. The relay
logs a `WARN` on every startup. The dashboard skips the token prompt.
- **`BUZZ_ADMIN_AUTH=nip98`:** NIP-98 HTTP Auth. Every request must carry an
`Authorization: Nostr <base64 event>` header containing a signed kind-27235
event. Authorized principals resolve from `RELAY_OPERATOR_PUBKEYS`
(comma-separated 64-char hex pubkeys for config-backed Operators),
`RELAY_OWNER_PUBKEY` (implicit Operator fallback when `RELAY_OPERATOR_PUBKEYS`
is unset), and the `relay_operators` table (DB-managed Operator/Moderator
roster). The dashboard requires a NIP-07 browser extension (nos2x or Alby);
without one it shows an installation screen. Individual operator access is
revocable without rotating a shared secret.
- Any unrecognised value for `BUZZ_ADMIN_AUTH` is a startup error
(typo-proofing). `BUZZ_ADMIN_TOKEN` set alongside `disabled` or `nip98` is
also a startup error.
- `Host`/`Origin` matching is retained in all modes as defense-in-depth.
- **Migration from the previous `BUZZ_ADMIN_INSECURE_NO_AUTH=true`:** replace
with `BUZZ_ADMIN_AUTH=disabled`. The behavior is identical.
- The relay NIP-11 relay-information document now advertises the admin API
origin in an optional `admin_api` field (`scheme://host[:port]`) whenever the
admin surface is configured (`BUZZ_ADMIN_HOST` set). The scheme follows the
same loopback rule as NIP-98 `u`-tag verification (`http` for
`localhost`/`127.x`/`[::1]`, else `https`). Clients can auto-discover the admin
console instead of requiring manual URL entry; the field is omitted entirely
when no admin surface is configured. IPv6 admin hosts must be bracketed
(`[::1]`, `[::1]:3000`); an unbracketed literal is a startup error because it
cannot form a valid URI authority.
- `RELAY_OPERATOR_API_ORIGIN` is no longer required at boot when
`RELAY_OPERATOR_PUBKEYS` is set. The allowlist is shared by the NIP-98 admin
console (which needs no origin) and the community-provisioning endpoints
(which do). Setting the pubkeys for the admin console no longer forces an
origin; the relay logs a `WARN` naming the affected feature, and the
community-provisioning endpoints (`POST /operator/communities`) fail closed
at request time until `RELAY_OPERATOR_API_ORIGIN` is set.

## v0.5.17

### Desktop and shared changes
Expand Down Expand Up @@ -232,7 +275,6 @@
- infra: bind development services to loopback ([#4871](https://github.com/block/buzz/pull/4871)) ([`65834d68d0d3441c4e628540d6d5c8b0a2e757c9`](https://github.com/block/buzz/commit/65834d68d0d3441c4e628540d6d5c8b0a2e757c9))

[Compare desktop-v0.5.7...desktop-v0.5.8](https://github.com/block/buzz/compare/desktop-v0.5.7...desktop-v0.5.8)

## v0.5.7

### Desktop and shared changes
Expand Down
24 changes: 23 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,24 @@ test-unit:
# `cargo test --workspace`; without this step a manifest edit that
# diverges Rust from the corpus ships green.
cargo nextest run -p buzz-agent --lib
# Admin API auth-boundary tests (api::admin in buzz-relay): the NIP-98
# duplicate-tag rejections, the Host/Origin replay-ordering causal pair,
# the admin.localhost origin/advertisement/canonical-URL pins, and the
# host-oracle/credential-first checks. These are the regression guard for
# the /api/admin/v1 moderation auth surface. Enumerated explicitly because
# nothing in CI runs `cargo test --workspace`, `just test-unit` did not
# enumerate `buzz-relay --lib`, and Backend Integration selects only the
# #[ignore]d Postgres suites — so these non-ignored tests ran in no lane
# and a red one could ship green (exactly how a broken admin test slipped
# past every gate once). Scoped to api::admin, not the whole buzz-relay
# --lib, because api::media has non-ignored tests that require Postgres.
# Two api::admin tests are excluded: they exercise a read-route DB
# fallthrough and pass without a database only by waiting out the sqlx
# acquire timeout (~30s each), so they belong to the Postgres lane, not
# the infra-free unit job — their Host/Origin gating is covered here by
# disabled_mode_still_requires_the_correct_host / _a_matching_origin.
cargo nextest run -p buzz-relay --lib \
-E 'test(/^api::admin::/) - test(=api::admin::tests::disabled_mode_allows_unauthenticated_requests_on_the_admin_host) - test(=api::admin::tests::nip98_mode_unrostered_signer_does_not_consume_a_replay_slot)'
else
./scripts/run-tests.sh unit
fi
Expand Down Expand Up @@ -443,7 +461,11 @@ admin: bootstrap _ensure-migrations
pnpm -C admin-web build
export BUZZ_ADMIN_HOST="${BUZZ_ADMIN_HOST:-admin.localhost:3000}"
export BUZZ_ADMIN_WEB_DIR="${BUZZ_ADMIN_WEB_DIR:-{{justfile_directory()}}/admin-web/dist}"
# The relay refuses to start without a token, and never logs one. Mint a
# throwaway per run so no dev secret is ever committed or reused.
export BUZZ_ADMIN_TOKEN="${BUZZ_ADMIN_TOKEN:-$(openssl rand -hex 32)}"
echo "Admin dashboard: http://${BUZZ_ADMIN_HOST}/reports"
echo "Admin token (paste into dashboard prompt): ${BUZZ_ADMIN_TOKEN}"
cargo run -p buzz-relay

# Seed deterministic reports and product feedback for local admin dashboard review
Expand All @@ -456,7 +478,7 @@ admin-check: fmt-check
cargo test -p buzz-relay api::admin
cargo test -p buzz-relay router::tests
pnpm -C admin-web check
pnpm -C admin-web exec playwright test
pnpm -C admin-web test:e2e

# Start the relay server in release mode
relay-release: _ensure-migrations
Expand Down
Loading
Loading