feat(relay): add operator community provisioning#1657
Conversation
4fecafd to
5301f3d
Compare
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
5301f3d to
de5fd17
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de5fd17d24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "POST", | ||
| "/operator/communities", | ||
| None, | ||
| Some(&body), |
There was a problem hiding this comment.
Require a signed payload for provisioning bodies
When an operator client signs this POST without a NIP-98 payload tag, verify_bridge_auth still accepts the auth event and only binds the URL/method, leaving the JSON body (host and initial_owner_pubkey) unsigned. In that case, anyone who obtains a fresh Authorization: Nostr ... header can race the legitimate request with a different body and create or rotate a community before the replay guard records the event; the operator provisioning endpoint should reject body-bearing requests unless the auth event includes a matching payload hash.
Useful? React with 👍 / 👎.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 found three issues worth fixing before merge:
-
high: require signed payloads for provisioning bodies
POST /operator/communitiesaccepts NIP-98 auth without requiring apayloadtag. In that case the auth event binds only URL + method, leavinghostandinitial_owner_pubkeyunsigned. Anyone who gets a freshAuthorization: Nostr ...header can race it with a different body and create or rotate a community before the replay guard records the event. Please reject body-bearing provisioning requests unless the NIP-98 event includes a matching payload hash. -
medium: validate community hosts as real authorities before storing
validate_hostchecks normalization and a few forbidden characters, but still accepts syntactically invalid authorities like:, malformed bracketed IPv6,example..com, labels with underscores, or overlong labels. Those can be inserted intocommunities.hosteven though they may never correspond to a valid HTTP Host authority. Please parse/validate ashost[:port]and store only canonical normalized authorities. -
medium: avoid reporting
createdafter a racing upsert conflictProvisioning computes
existedwith a separate lookup beforeensure_configured_community. Two concurrent operator requests can both observeexisted = false; one inserts, the other hits the upsert conflict but still returnsstatus: "created". Please make the DB helper return whether the insert actually won, or do the check+insert in one transaction/CTE.
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
Why
Enable kgoose or another deployment operator to provision and inspect Buzz communities without direct Postgres access, while preserving the relay's host-derived tenant boundary and keeping the Nostr event data plane unchanged.
What
POST /operator/communitiesas a standalone NIP-98-signed operator endpointGET /operator/communities?owner_pubkey=<hex>for kgoose's list-owned-communities readGET /operator/communities/availability?host=<host>so the relay is the source of truth for host normalization and availability checksRELAY_OPERATOR_PUBKEYS, fail closed by default, and hard-error on malformed configured pubkeyscommunitiesensure/create, optional owner bootstrap, and availability host normalizationinitial_owner_pubkeyon an existing community rotates the owner via the same bootstrap path asRELAY_OWNER_PUBKEY9037, no relay-membership bypass in/events, no ingest routing carve-out, no storage/fan-outRisk Assessment
Medium — this adds a deployment-root authority surface. The blast radius is bounded by NIP-98 request signing, replay protection, explicit
RELAY_OPERATOR_PUBKEYSallowlist, strict host/pubkey validation, and a no-op default config. Keeping this as a separate operator endpoint avoids special cases in the tenant-scoped event ingest path.References
cargo fmtcargo fmt --checkcargo check -p buzz-relaycargo test -p buzz-core no_duplicate_kind_values --libcargo test -p buzz-relay community_provisioning --libcargo test -p buzz-relay relay_operator_pubkeys --libcargo test -p buzz-db list_communities_owned_by_returns_only_owner_rows --lib(ignored by default as Postgres-dependent)sadscan,rust-fmt,web-fix,desktop-tauri-fmt,desktop-fix,mobile-fixcheck-push-org,desktop-test,mobile-test,desktop-tauri-test,rust-testsGenerated with Codex