Skip to content

Commit 0b18b2b

Browse files
author
Duncan
committed
Merge remote-tracking branch 'origin/main' into duncan/phase1-merged-agent-edit
* origin/main: Refresh mobile utility surfaces and theme picker (#6944) fix(desktop): complete project empty and context states (#6980) Fix mobile jump-to-latest flicker (#6807) refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777) refactor(db): split channel membership store (#6782) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2 parents fdd41e5 + ed11c8d commit 0b18b2b

111 files changed

Lines changed: 26448 additions & 4671 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,47 @@ RELAY_URL=ws://localhost:3000
5959
# (use `just web` for Vite HMR instead).
6060
# BUZZ_WEB_DIR=./web/dist
6161

62+
# -----------------------------------------------------------------------------
63+
# Admin Dashboard (private moderation surface)
64+
# -----------------------------------------------------------------------------
65+
# Host name that serves the moderation dashboard and its /api/admin/v1
66+
# endpoints. Leave unset to keep the admin surface absent.
67+
# BUZZ_ADMIN_HOST=admin.localhost:3000
68+
#
69+
# Authentication mode. Accepted values: nip98 (default), disabled.
70+
# Any other value is a startup error. Token authentication was removed:
71+
# BUZZ_ADMIN_TOKEN is ignored with a startup warning — remove it from the environment.
72+
# BUZZ_ADMIN_AUTH=nip98
73+
#
74+
# Option A — BUZZ_ADMIN_AUTH=nip98 (Nostr pubkey-based auth, default):
75+
# NIP-98 HTTP Auth. Each request must carry an Authorization: Nostr header
76+
# with a signed kind-27235 event. Authorized principals are resolved from:
77+
# 1. RELAY_OPERATOR_PUBKEYS — comma-separated 64-char hex pubkeys (config Operators).
78+
# 2. RELAY_OWNER_PUBKEY — implicit Operator fallback when RELAY_OPERATOR_PUBKEYS is unset.
79+
# 3. relay_operators table — DB-managed Operator/Moderator roster.
80+
# The dashboard requires a NIP-07 browser extension.
81+
# Setting RELAY_OPERATOR_PUBKEYS for the admin console does NOT require
82+
# RELAY_OPERATOR_API_ORIGIN; that origin is only for community provisioning
83+
# (see below). When BUZZ_ADMIN_HOST is set, the relay advertises the admin
84+
# origin in its NIP-11 document (`admin_api` field) so clients can auto-discover
85+
# the console without manual URL entry.
86+
# RELAY_OPERATOR_PUBKEYS=<64-char hex pubkey>[,<64-char hex pubkey>...]
87+
#
88+
# Option B — BUZZ_ADMIN_AUTH=disabled (network-layer auth only):
89+
# Set only when the admin API is already protected at the network layer
90+
# (VPN, private ingress). The relay logs a WARN on every startup.
91+
# `just admin` defaults to this mode for local review.
92+
#
93+
# Directory holding the built dashboard assets (`pnpm -C admin-web build`).
94+
# BUZZ_ADMIN_WEB_DIR=./admin-web/dist
95+
#
96+
# Canonical origin (http(s)://host[:port], no path) that community-provisioning
97+
# NIP-98 requests are verified against. Required only to USE the provisioning
98+
# endpoints (POST /operator/communities) — not for the admin console. When
99+
# RELAY_OPERATOR_PUBKEYS is set but this is unset, the relay boots with a WARN
100+
# and provisioning requests fail closed until it is set.
101+
# RELAY_OPERATOR_API_ORIGIN=http://127.0.0.1:3000
102+
62103
# Optional relay-owned KLIPY key. When set, NIP-11 advertises GIF search and
63104
# authenticated desktop clients use this relay as the metadata/search proxy.
64105
# Keep the real value in your deployment's secret manager; never commit it.

.github/workflows/ci.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,77 @@ jobs:
780780
--run-ignored ignored-only
781781
env:
782782
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
783+
- name: Admin API nip98 read-write attribution test
784+
# The only real HTTP → nip98 operator principal → mutation → cross-table
785+
# attribution coverage: an authenticated operator's dismiss attributes
786+
# to the operator's own key with relay_operator authority. Staffing
787+
# PUT/DELETE attribution is covered by
788+
# nip98_staffing_put_and_delete_write_attributed_audit_rows in the
789+
# roster-audit lane below. #[ignore]d in the default suite — see
790+
# api::admin::tests::nip98_operator_dismiss_succeeds_attributed_to_operator.
791+
run: |
792+
cargo nextest run \
793+
--archive-file target/ci/backend-integration-tests.tar.zst \
794+
-E 'package(buzz-relay) and test(=api::admin::tests::nip98_operator_dismiss_succeeds_attributed_to_operator)' \
795+
--run-ignored ignored-only
796+
env:
797+
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
798+
- name: Admin API unrostered-signer replay invariant
799+
# The only causal proof that a validly-signing but unrostered key cannot
800+
# consume NIP-98 replay slots: it asserts principal resolution fails
801+
# BEFORE the replay ID is claimed (tracking.claim_count() == 0). This
802+
# test is non-ignored, so it runs neither in Backend Integration's
803+
# ignored-only selectors nor in the infra-free unit job — the unit job's
804+
# api::admin selector excludes it because DB-free it only passes by
805+
# waiting out the ~30s sqlx acquire timeout on a read-route fallthrough.
806+
# It lives here so a reachable Postgres resolves (and fails) the lookup
807+
# fast instead of timing out.
808+
run: |
809+
cargo nextest run \
810+
--archive-file target/ci/backend-integration-tests.tar.zst \
811+
-E 'package(buzz-relay) and test(=api::admin::tests::nip98_mode_unrostered_signer_does_not_consume_a_replay_slot)'
812+
env:
813+
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
814+
- name: Admin API roster-audit / timeout / canonicalization security tests
815+
# Security-review fixes for the roster admin API, all #[ignore]d in the
816+
# default suite (they need Postgres) and selected by no other job:
817+
# - buzz-db relay_operators::tests: audit pre-image trail, per-target
818+
# lock serialization, insertion-time audit ordering, and
819+
# audit-failure rollback coupling.
820+
# - buzz-db relay_operators::tests last-operator invariant: sole DB
821+
# operator cannot self-demote or self-delete to zero, config presence
822+
# lifts the guard, and concurrent cross-target deletes racing to zero
823+
# leave exactly one operator (roster-wide advisory lock).
824+
# - buzz-relay api::admin: NIP-98 staffing writes attributed audit rows,
825+
# adversarial expirationSecs rejected at the resolve route, mixed-case
826+
# staffing normalizes to one canonical row.
827+
#
828+
# --test-threads=1: the last-operator invariant counts the roster
829+
# globally, and the sole-operator tests clear the roster then assert
830+
# their operator is the only one. They must not race each other on the
831+
# shared test roster, so this lane runs serially.
832+
run: |
833+
cargo nextest run \
834+
--archive-file target/ci/backend-integration-tests.tar.zst \
835+
--test-threads=1 \
836+
-E '(package(buzz-db) and test(=relay_operators::tests::roster_mutations_write_pre_image_audit_rows)) or (package(buzz-db) and test(=relay_operators::tests::concurrent_upserts_serialize_and_record_true_pre_image)) or (package(buzz-db) and test(=relay_operators::tests::audit_order_follows_seq_under_backward_clock)) or (package(buzz-db) and test(=relay_operators::tests::audit_insert_failure_rolls_back_roster_mutation)) or (package(buzz-db) and test(=relay_operators::tests::demoting_sole_db_operator_without_config_is_rejected)) or (package(buzz-db) and test(=relay_operators::tests::deleting_sole_db_operator_without_config_is_rejected)) or (package(buzz-db) and test(=relay_operators::tests::config_present_allows_deleting_last_db_operator)) or (package(buzz-db) and test(=relay_operators::tests::concurrent_deletes_racing_to_zero_leave_one_operator)) or (package(buzz-relay) and test(=api::admin::tests::nip98_staffing_put_and_delete_write_attributed_audit_rows)) or (package(buzz-relay) and test(=api::admin::tests::resolve_route_rejects_adversarial_expiration_and_leaves_report_open)) or (package(buzz-relay) and test(=api::admin::tests::mixed_case_non_config_staffing_normalizes_to_one_row))' \
837+
--run-ignored ignored-only
838+
env:
839+
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
840+
- name: Admin API escalation-scoping tests
841+
# Escalation scoping for the moderation queue, all #[ignore]d (they need
842+
# Postgres) and selected by no other job:
843+
# - GET /reports defaults to the escalated-only backstop, scope=all
844+
# restores full visibility, explicit status= overrides the default.
845+
# - member reports with category 'illegal' auto-escalate at ingestion
846+
# while every other category still lands 'open'.
847+
run: |
848+
cargo nextest run \
849+
--archive-file target/ci/backend-integration-tests.tar.zst \
850+
-E '(package(buzz-relay) and test(=api::admin::tests::reports_default_lists_escalated_only)) or (package(buzz-relay) and test(=api::admin::tests::reports_scope_all_lists_every_status)) or (package(buzz-relay) and test(=api::admin::tests::reports_explicit_status_filter_overrides_default)) or (package(buzz-db) and test(=moderation::tests::illegal_report_auto_escalates_at_ingest)) or (package(buzz-db) and test(=moderation::tests::non_illegal_report_lands_open_at_ingest)) or (package(buzz-db) and test(=relay_admin_actions::tests::auto_escalated_report_reopens_like_an_admin_escalated_one))' \
851+
--run-ignored ignored-only
852+
env:
853+
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
783854
- name: Upload relay log
784855
if: failure()
785856
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7

Justfile

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,31 @@ test-unit:
354354
# `cargo test --workspace`; without this step a manifest edit that
355355
# diverges Rust from the corpus ships green.
356356
cargo nextest run -p buzz-agent --lib
357+
# Admin API auth-boundary tests (api::admin in buzz-relay): the NIP-98
358+
# duplicate-tag rejections, the Host/Origin replay-ordering causal pair,
359+
# the admin.localhost origin/advertisement/canonical-URL pins, and the
360+
# host-oracle/credential-first checks. These are the regression guard for
361+
# the /api/admin/v1 moderation auth surface. Enumerated explicitly because
362+
# nothing in CI runs `cargo test --workspace`, `just test-unit` did not
363+
# enumerate `buzz-relay --lib`, and Backend Integration selects only the
364+
# #[ignore]d Postgres suites — so these non-ignored tests ran in no lane
365+
# and a red one could ship green (exactly how a broken admin test slipped
366+
# past every gate once). Scoped to api::admin, not the whole buzz-relay
367+
# --lib, because api::media has non-ignored tests that require Postgres.
368+
# Two api::admin tests are excluded: both exercise a read-route DB
369+
# fallthrough and pass without a database only by waiting out the sqlx
370+
# acquire timeout (~30s each), so they do not belong in the infra-free
371+
# unit job. nip98_mode_unrostered_signer_does_not_consume_a_replay_slot
372+
# asserts a unique replay-guard invariant, so it is wired into the
373+
# Postgres-backed Backend Integration job (see ci.yml "Admin API
374+
# unrostered-signer replay invariant"). disabled_mode_allows_
375+
# unauthenticated_requests_on_the_admin_host has no unique invariant:
376+
# disabled-mode unauthenticated success is covered by
377+
# disabled_mode_regression_pin_unauthenticated_request_is_served on the
378+
# DB-free /probe route, and its Host/Origin gating is covered here by
379+
# disabled_mode_still_requires_the_correct_host / _a_matching_origin.
380+
cargo nextest run -p buzz-relay --lib \
381+
-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)'
357382
else
358383
./scripts/run-tests.sh unit
359384
fi
@@ -454,7 +479,7 @@ relay-web: bootstrap _ensure-migrations
454479
pnpm -C web build
455480
BUZZ_WEB_DIR=./web/dist cargo run -p buzz-relay
456481
457-
# Build and run the private read-only admin dashboard
482+
# Build and run the private admin dashboard
458483
admin: bootstrap _ensure-migrations
459484
#!/usr/bin/env bash
460485
set -euo pipefail
@@ -466,20 +491,26 @@ admin: bootstrap _ensure-migrations
466491
pnpm -C admin-web build
467492
export BUZZ_ADMIN_HOST="${BUZZ_ADMIN_HOST:-admin.localhost:3000}"
468493
export BUZZ_ADMIN_WEB_DIR="${BUZZ_ADMIN_WEB_DIR:-{{justfile_directory()}}/admin-web/dist}"
494+
# Default to disabled auth locally: localhost is the network boundary and a
495+
# NIP-07 signer extension can't be assumed in dev. Override per run with
496+
# BUZZ_ADMIN_AUTH=nip98 (plus RELAY_OPERATOR_PUBKEYS or RELAY_OWNER_PUBKEY)
497+
# to exercise the authenticated path.
498+
export BUZZ_ADMIN_AUTH="${BUZZ_ADMIN_AUTH:-disabled}"
469499
echo "Admin dashboard: http://${BUZZ_ADMIN_HOST}/reports"
500+
echo "Auth mode: ${BUZZ_ADMIN_AUTH} (set BUZZ_ADMIN_AUTH=nip98 to require a signed operator)"
470501
cargo run -p buzz-relay
471502
472503
# Seed deterministic reports and product feedback for local admin dashboard review
473504
admin-seed: _ensure-migrations
474505
./scripts/seed-admin-dashboard.sh
475506

476-
# Run focused relay and browser checks for the read-only admin dashboard
507+
# Run focused relay and browser checks for the admin dashboard
477508
admin-check: fmt-check
478509
cargo check -p buzz-relay --all-targets
479510
cargo test -p buzz-relay api::admin
480511
cargo test -p buzz-relay router::tests
481512
pnpm -C admin-web check
482-
pnpm -C admin-web exec playwright test
513+
pnpm -C admin-web test:e2e
483514

484515
# Start the relay server in release mode
485516
relay-release: bootstrap _ensure-migrations

0 commit comments

Comments
 (0)