Skip to content

Commit 3d7627e

Browse files
committed
Merge remote-tracking branch 'origin/main' into kennylopez-welcome-message-builder
* origin/main: refactor(db): extract domain stores from database runtime (#6987) feat(desktop): add team sharing to community catalog (#3995) 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) feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776) Signed-off-by: Clay Delk <clay.delk@gmail.com>
2 parents c12f580 + a373078 commit 3d7627e

185 files changed

Lines changed: 42570 additions & 12234 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

0 commit comments

Comments
 (0)