feat(relay): gRPC event-admission (nauthz) service enforcing the write policy

nostr-rs-relay's [grpc] event_admission_server calls Authorization.EventAdmit
once per inbound event; we answer PERMIT/DENY so the write policy is enforced at
ingest (dynamic, per-event — a static pubkey whitelist can't do inbox/PoW).

- proto/nauthz.proto (matches nostr-rs-relay's contract) compiled by build.rs;
  protoc is vendored (protoc-bin-vendored) so a source build needs no system
  protobuf-compiler. tonic/prost added (reuse the hyper/tower/h2 stack axum
  already pulls in); server-only codegen.
- infra/relay/nauthz: AdmissionService resolves author + recipient (p-tag)
  registration via the DB, runs the pure policy engine, returns the decision;
  served on a loopback socket (spawned in main only when relay enabled + policy
  != open), mirroring the admin plane. Fail-closed on a malformed request.
- db: is_registered_npub(hex) — a no-PII membership check safe to call per event.
- tests/relay_admission.rs: L1 handler tests (is_registered_npub membership +
  outbox/inbox/deny-non-giftwrap/deny-unregistered-recipient through the real
  event_admit handler against live Postgres).
This commit is contained in:
jwinterm
2026-07-02 06:46:59 -04:00
parent a8f1a9e50c
commit 63d2c75194
9 changed files with 658 additions and 5 deletions
+25
View File
@@ -231,6 +231,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
// Nostr relay event-admission (nauthz) gRPC service — only for a non-`open`
// write policy (open needs no admission). Loopback socket, like the admin
// plane; nostr-rs-relay is configured to call it per inbound event.
if let Some(relay) = state.relay.clone() {
use smirk_backend_core::infra::relay::WritePolicy;
if relay.write_policy() != WritePolicy::Open {
let addr: std::net::SocketAddr = state
.config
.messaging
.relay
.admission_bind
.parse()
.map_err(|e| format!("invalid RELAY_ADMISSION_BIND: {e}"))?;
let db = state.db.clone();
tracing::info!("relay event-admission (nauthz) listening on {addr}");
tokio::spawn(async move {
if let Err(e) =
smirk_backend_core::infra::relay::nauthz::serve(addr, relay, db).await
{
tracing::error!(error = %e, "relay admission service exited");
}
});
}
}
let app = build_router(state);
let listener = tokio::net::TcpListener::bind(&addr).await?;