13 KiB
NIP-86 Relay Management API Implementation Research
Date: 2026-01-15
nostr-sdk Version: 0.43 (git rev 4767ad13)
Purpose: Research NIP-86 support in nostr-sdk and determine implementation approach for ngit-grasp
Executive Summary
NIP-86 defines a relay management API using JSON-RPC over HTTP with NIP-98 authentication. nostr-sdk 0.43 provides NIP-98 HTTP auth support but does NOT provide NIP-86 relay management API components. We will need to implement the NIP-86 JSON-RPC protocol from scratch, but can leverage nostr-sdk's NIP-98 authentication.
NIP-86 Specification Overview
Protocol Design
NIP-86 defines a management API for Nostr relays with the following characteristics:
Transport: HTTP POST to the same URI as the WebSocket relay endpoint
Content-Type: application/nostr+json+rpc
Authentication: NIP-98 HTTP Auth (kind 27235 events in Authorization header)
Protocol: JSON-RPC-like request/response format
Request Format
{
"method": "<method-name>",
"params": ["<array>", "<of>", "<parameters>"]
}
Response Format
{
"result": {"<arbitrary>": "<value>"},
"error": "<optional error message, if the call has errored>"
}
Management Methods
NIP-86 defines the following standard methods relevant to ngit-grasp:
Blacklist/Allow Operations:
banpubkey- Ban a public keylistbannedpubkeys- List all banned public keysallowpubkey- Remove ban from public keylistallowedpubkeys- List explicitly allowed public keysbanevent- Ban an eventlistbannedevents- List banned eventsallowevent- Remove event ban
Relay Configuration:
changerelayname- Update relay namechangerelaydescription- Update relay descriptionchangerelayicon- Update relay icon URL
Event Kind Filtering:
allowkind- Allow an event kinddisallowkind- Disallow an event kindlistallowedkinds- List allowed kinds
IP Blocking:
blockip- Block IP addressunblockip- Unblock IP addresslistblockedips- List blocked IPs
Discovery:
supportedmethods- List all supported methods
Authentication Requirements
Per NIP-86, all requests must include:
- Authorization header containing a NIP-98 event (kind 27235)
- The NIP-98 event must include:
utag: Exact request URL (including query params)methodtag: HTTP method used (POST for NIP-86)payloadtag: SHA256 hash of request body (required for POST/PUT/PATCH)
- Timestamp validation (suggested 60 second window)
What nostr-sdk 0.43 Provides
NIP-98 HTTP Auth Support (AVAILABLE)
nostr-sdk 0.43 includes full NIP-98 implementation in the nip98 module:
Location: crates/nostr/src/nips/nip98.rs
Available Types:
pub enum HttpMethod { GET, POST, PUT, PATCH }
pub struct HttpData {
pub url: Url,
pub method: HttpMethod,
pub payload: Option<Sha256Hash>,
}
impl HttpData {
pub fn new(url: Url, method: HttpMethod) -> Self
pub fn payload(self, payload: Sha256Hash) -> Self
// Build Authorization header (requires "rand" feature)
pub async fn to_authorization<T>(self, signer: &T) -> Result<String, Error>
where T: NostrSigner
}
// Server-side verification
pub fn verify_auth_header(
auth_header: &str,
url: &Url,
method: HttpMethod,
current_time: Timestamp,
body: Option<&[u8]>,
) -> Result<PublicKey, Error>
EventBuilder Support:
impl EventBuilder {
#[cfg(feature = "nip98")]
pub fn http_auth(data: HttpData) -> Self
}
Kind Support:
Kind::HttpAuth => 27235 // Defined in event/kind.rs
Features Required:
nip98- Enables NIP-98 support (included in nostr-sdk dependencies)std- Standard library support (already enabled)rand- For signing (already enabled)
What's NOT Provided (Need to Implement)
NIP-86 specific components NOT in nostr-sdk:
-
JSON-RPC Protocol Handling
- Request/response types
- Method dispatch
- Error handling
- Parameter validation
-
Management Methods
- No blacklist operations
- No quota management
- No repository operations
- No relay configuration
- No method discovery
-
HTTP Endpoint Handler
- Content-Type detection
- Route differentiation (WebSocket vs Management API)
- Request body parsing
Implementation Approach
Architecture
HTTP Request (POST /)
├─ Content-Type: application/nostr+json+rpc?
│ ├─ YES → NIP-86 Management Handler
│ │ ├─ Extract Authorization header
│ │ ├─ Verify NIP-98 auth (using nostr-sdk)
│ │ ├─ Parse JSON-RPC request
│ │ ├─ Dispatch to method handler
│ │ └─ Return JSON-RPC response
│ └─ NO → Check for Upgrade: websocket
│ ├─ YES → Nostr Relay (existing)
│ └─ NO → HTTP handlers (existing)
Phase 1: Core Infrastructure
File: src/nip86/mod.rs
use nostr_sdk::prelude::*;
use nostr_sdk::nips::nip98;
use serde::{Deserialize, Serialize};
/// NIP-86 JSON-RPC request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagementRequest {
pub method: String,
pub params: Vec<serde_json::Value>,
}
/// NIP-86 JSON-RPC response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagementResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl ManagementResponse {
pub fn success(result: serde_json::Value) -> Self {
Self { result: Some(result), error: None }
}
pub fn error<S: Into<String>>(message: S) -> Self {
Self { result: None, error: Some(message.into()) }
}
}
/// Verify NIP-98 auth and extract authorized public key
pub fn verify_management_auth(
auth_header: &str,
url: &Url,
body: &[u8],
config: &ManagementConfig,
) -> Result<PublicKey, ManagementError> {
let pubkey = nip98::verify_auth_header(
auth_header,
url,
nip98::HttpMethod::POST,
Timestamp::now(),
Some(body),
)?;
// Check if pubkey is authorized admin
if !config.is_authorized_admin(&pubkey) {
return Err(ManagementError::Unauthorized);
}
Ok(pubkey)
}
Phase 2: HTTP Handler Integration
File: src/http/management.rs
use hyper::{Body, Request, Response, StatusCode};
use nostr_sdk::Url;
const NIP86_CONTENT_TYPE: &str = "application/nostr+json+rpc";
pub async fn handle_management_request(
req: Request<Body>,
relay_url: &Url,
config: &ManagementConfig,
) -> Result<Response<Body>, hyper::Error> {
// Extract Authorization header
let auth_header = req
.headers()
.get("authorization")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| /* 401 Unauthorized */)?;
// Read body
let body_bytes = hyper::body::to_bytes(req.into_body()).await?;
// Verify NIP-98 authentication
let admin_pubkey = match verify_management_auth(
auth_header,
relay_url,
&body_bytes,
config,
) {
Ok(pk) => pk,
Err(_) => return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(Body::from("Unauthorized"))
.unwrap()),
};
// Parse JSON-RPC request
let request: ManagementRequest = serde_json::from_slice(&body_bytes)
.map_err(|_| /* 400 Bad Request */)?;
// Dispatch to method handler
let response = dispatch_method(request, admin_pubkey, config).await;
// Return JSON response
Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&response).unwrap()))
.unwrap())
}
async fn dispatch_method(
request: ManagementRequest,
_admin: PublicKey,
_config: &ManagementConfig,
) -> ManagementResponse {
match request.method.as_str() {
"supportedmethods" => {
ManagementResponse::success(json!([
"supportedmethods",
"banpubkey",
"listbannedpubkeys",
// ... etc
]))
}
"banpubkey" => {
// Implementation
todo!()
}
_ => ManagementResponse::error(format!(
"Unknown method: {}",
request.method
)),
}
}
Phase 3: Method Implementations
File: src/nip86/methods/mod.rs
Implement each NIP-86 method:
pub mod blacklist;
pub mod configuration;
pub mod quota; // Extension for ngit-grasp
pub mod repository; // Extension for ngit-grasp
pub trait ManagementMethod {
fn name(&self) -> &str;
fn execute(&self, params: Vec<Value>) -> Result<Value, String>;
}
Phase 4: Configuration
Update: src/config.rs, docs/reference/configuration.md, nix/module.nix, .env.example
# NIP-86 Management API Configuration
NGIT_MANAGEMENT_ENABLED=true
NGIT_MANAGEMENT_ADMIN_PUBKEYS=npub1...,npub2...
# Optional: Require NIP-98 payload hash verification
NGIT_MANAGEMENT_REQUIRE_PAYLOAD_HASH=true
Implementation Checklist
Core NIP-86 Support
- Create
src/nip86/mod.rsmodule - Define
ManagementRequestandManagementResponsetypes - Implement auth verification using
nip98::verify_auth_header - Add HTTP handler for
application/nostr+json+rpcContent-Type - Implement method dispatcher
- Add configuration for admin public keys
Standard NIP-86 Methods
supportedmethods- Method discoverybanpubkey/allowpubkey- Public key blacklistlistbannedpubkeys/listallowedpubkeys- List blacklistsbanevent/allowevent- Event blacklistlistbannedevents- List banned eventschangerelayname- Update NIP-11 namechangerelaydescription- Update NIP-11 descriptionchangerelayicon- Update NIP-11 icon
ngit-grasp Extensions (Beyond NIP-86)
setuserstoragequota- Set per-user quotasetrepositorystoragequota- Set per-repo quotalistquotas- List all quotasdeletequota- Remove quotadeleterepository- Delete repository and git datagenerateapikey- Generate Prometheus API keylistapikeys- List API keysrevokeapikey- Revoke API key
Integration
- Update
src/http/mod.rsto route NIP-86 requests - Add blacklist checks to event acceptance policy
- Persist blacklists/quotas to database
- Add metrics for management operations
- Write integration tests
Documentation
- Update
docs/reference/configuration.mdwith NIP-86 config - Create
docs/how-to/manage-relay-nip86.md - Document custom ngit-grasp extensions
- Add examples of management API usage
Testing Strategy
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_verify_auth_valid() {
// Create valid NIP-98 auth header
// Verify it passes
}
#[tokio::test]
async fn test_verify_auth_expired() {
// Create expired NIP-98 auth
// Verify it fails
}
#[tokio::test]
async fn test_dispatch_unknown_method() {
// Request unknown method
// Verify error response
}
}
Integration Tests
#[tokio::test]
async fn test_ban_pubkey_via_nip86() {
let relay = TestRelay::start().await;
let admin_keys = Keys::generate();
// Configure admin
// Send NIP-86 banpubkey request
// Verify user is banned
// Attempt to publish event
// Verify rejection
}
Effort Estimation
If implementing from scratch:
- Phase 1 (Core): 1-2 days
- Phase 2 (HTTP Handler): 0.5-1 day
- Phase 3 (Standard Methods): 2-3 days
- Phase 4 (Config & Docs): 0.5-1 day
- ngit-grasp Extensions: 1-2 days
- Testing: 1-2 days
Total: 6-11 days (depends on complexity of quota/blacklist persistence)
Alternative: REST API
If NIP-86 proves too complex or has poor tooling support, consider:
Pros of REST alternative:
- More familiar HTTP patterns
- Better tooling (OpenAPI, etc.)
- Easier to test with curl/httpie
Cons:
- Loses Nostr-native authentication
- Requires separate API key system
- Not standardized in Nostr ecosystem
Recommendation: Implement NIP-86 since:
- NIP-98 auth is already available in nostr-sdk
- Standardization benefits the Nostr ecosystem
- No separate API key management needed
- Aligns with "Nostr-native" philosophy
References
- NIP-86 Spec: https://github.com/nostr-protocol/nips/blob/master/86.md
- NIP-98 Spec: https://github.com/nostr-protocol/nips/blob/master/98.md
- nostr-sdk NIP-98 impl: https://github.com/rust-nostr/nostr/blob/4767ad13/crates/nostr/src/nips/nip98.rs
- nostr-sdk EventBuilder: https://github.com/rust-nostr/nostr/blob/4767ad13/crates/nostr/src/event/builder.rs
Next Steps
- Review this document with stakeholders
- Decide on standard NIP-86 methods vs ngit-grasp extensions
- Create implementation issue with detailed subtasks
- Begin Phase 1 implementation (core types and auth)
- Iterate with testing at each phase
Research completed by: AI Agent (File Search Specialist)
Review status: Pending human review