diff --git a/docs/research/nip-86-implementation-options.md b/docs/research/nip-86-implementation-options.md new file mode 100644 index 0000000..5e10fd6 --- /dev/null +++ b/docs/research/nip-86-implementation-options.md @@ -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": "", + "params": ["", "", ""] +} +``` + +### Response Format + +```json +{ + "result": {"": ""}, + "error": "" +} +``` + +### 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, +} + +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(self, signer: &T) -> Result + 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 +``` + +**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, +} + +/// NIP-86 JSON-RPC response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ManagementResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl ManagementResponse { + pub fn success(result: serde_json::Value) -> Self { + Self { result: Some(result), error: None } + } + + pub fn error>(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 { + 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, + relay_url: &Url, + config: &ManagementConfig, +) -> Result, 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) -> Result; +} +``` + +### 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