Files
ngit-grasp/docs/research/nip-86-implementation-options.md
T

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 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:

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:

  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

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.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

#[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:

  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

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