mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
docs: research NIP-86 support in nostr-sdk
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user