docs: research NIP-86 support in nostr-sdk

This commit is contained in:
DanConwayDev
2026-01-15 09:54:14 +00:00
parent 78707898fd
commit fdecc56b82
@@ -0,0 +1,485 @@
# 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
```json
{
"method": "<method-name>",
"params": ["<array>", "<of>", "<parameters>"]
}
```
### Response Format
```json
{
"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 key
- `listbannedpubkeys` - List all banned public keys
- `allowpubkey` - Remove ban from public key
- `listallowedpubkeys` - List explicitly allowed public keys
- `banevent` - Ban an event
- `listbannedevents` - List banned events
- `allowevent` - Remove event ban
**Relay Configuration:**
- `changerelayname` - Update relay name
- `changerelaydescription` - Update relay description
- `changerelayicon` - Update relay icon URL
**Event Kind Filtering:**
- `allowkind` - Allow an event kind
- `disallowkind` - Disallow an event kind
- `listallowedkinds` - List allowed kinds
**IP Blocking:**
- `blockip` - Block IP address
- `unblockip` - Unblock IP address
- `listblockedips` - List blocked IPs
**Discovery:**
- `supportedmethods` - List all supported methods
### Authentication Requirements
Per NIP-86, all requests must include:
1. **Authorization header** containing a NIP-98 event (kind 27235)
2. The NIP-98 event must include:
- `u` tag: Exact request URL (including query params)
- `method` tag: HTTP method used (POST for NIP-86)
- `payload` tag: SHA256 hash of request body (required for POST/PUT/PATCH)
3. 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:**
```rust
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:**
```rust
impl EventBuilder {
#[cfg(feature = "nip98")]
pub fn http_auth(data: HttpData) -> Self
}
```
**Kind Support:**
```rust
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:**
1. **JSON-RPC Protocol Handling**
- Request/response types
- Method dispatch
- Error handling
- Parameter validation
2. **Management Methods**
- No blacklist operations
- No quota management
- No repository operations
- No relay configuration
- No method discovery
3. **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`
```rust
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`
```rust
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:
```rust
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`
```bash
# 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.rs` module
- [ ] Define `ManagementRequest` and `ManagementResponse` types
- [ ] Implement auth verification using `nip98::verify_auth_header`
- [ ] Add HTTP handler for `application/nostr+json+rpc` Content-Type
- [ ] Implement method dispatcher
- [ ] Add configuration for admin public keys
### Standard NIP-86 Methods
- [ ] `supportedmethods` - Method discovery
- [ ] `banpubkey` / `allowpubkey` - Public key blacklist
- [ ] `listbannedpubkeys` / `listallowedpubkeys` - List blacklists
- [ ] `banevent` / `allowevent` - Event blacklist
- [ ] `listbannedevents` - List banned events
- [ ] `changerelayname` - Update NIP-11 name
- [ ] `changerelaydescription` - Update NIP-11 description
- [ ] `changerelayicon` - Update NIP-11 icon
### ngit-grasp Extensions (Beyond NIP-86)
- [ ] `setuserstoragequota` - Set per-user quota
- [ ] `setrepositorystoragequota` - Set per-repo quota
- [ ] `listquotas` - List all quotas
- [ ] `deletequota` - Remove quota
- [ ] `deleterepository` - Delete repository and git data
- [ ] `generateapikey` - Generate Prometheus API key
- [ ] `listapikeys` - List API keys
- [ ] `revokeapikey` - Revoke API key
### Integration
- [ ] Update `src/http/mod.rs` to 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.md` with 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
```rust
#[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
```rust
#[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:
1. NIP-98 auth is already available in nostr-sdk
2. Standardization benefits the Nostr ecosystem
3. No separate API key management needed
4. 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
1. Review this document with stakeholders
2. Decide on standard NIP-86 methods vs ngit-grasp extensions
3. Create implementation issue with detailed subtasks
4. Begin Phase 1 implementation (core types and auth)
5. Iterate with testing at each phase
---
**Research completed by:** AI Agent (File Search Specialist)
**Review status:** Pending human review