handle-nrc.go raw

   1  package app
   2  
   3  import (
   4  	"encoding/json"
   5  	"net/http"
   6  	"strings"
   7  
   8  	"next.orly.dev/pkg/lol/chk"
   9  	"next.orly.dev/pkg/lol/log"
  10  
  11  	"next.orly.dev/pkg/nostr/crypto/keys"
  12  	"next.orly.dev/pkg/nostr/encoders/hex"
  13  	"next.orly.dev/pkg/nostr/httpauth"
  14  	"next.orly.dev/pkg/acl"
  15  )
  16  
  17  // NRCConnectionResponse is the response structure for NRC connection API.
  18  type NRCConnectionResponse struct {
  19  	ID            string `json:"id"`
  20  	Label         string `json:"label"`
  21  	RendezvousURL string `json:"rendezvous_url"`
  22  	CreatedAt     int64  `json:"created_at"`
  23  	LastUsed      int64  `json:"last_used"`
  24  	URI           string `json:"uri,omitempty"` // Only included when specifically requested
  25  }
  26  
  27  // NRCConnectionsResponse is the response for listing all connections.
  28  type NRCConnectionsResponse struct {
  29  	Connections []NRCConnectionResponse `json:"connections"`
  30  	Config      NRCConfigResponse       `json:"config"`
  31  }
  32  
  33  // NRCConfigResponse contains NRC configuration status.
  34  type NRCConfigResponse struct {
  35  	Enabled       bool   `json:"enabled"`
  36  	RendezvousURL string `json:"rendezvous_url"`
  37  	RelayPubkey   string `json:"relay_pubkey"`
  38  }
  39  
  40  // NRCCreateRequest is the request body for creating a connection.
  41  type NRCCreateRequest struct {
  42  	Label         string `json:"label"`
  43  	RendezvousURL string `json:"rendezvous_url"` // WebSocket URL of the rendezvous relay
  44  }
  45  
  46  // handleNRCConnections handles GET /api/nrc/connections
  47  func (s *Server) handleNRCConnections(w http.ResponseWriter, r *http.Request) {
  48  	if r.Method != http.MethodGet {
  49  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  50  		return
  51  	}
  52  
  53  	// Validate NIP-98 authentication
  54  	valid, pubkey, err := httpauth.CheckAuth(r)
  55  	if chk.E(err) || !valid {
  56  		errorMsg := "NIP-98 authentication validation failed"
  57  		if err != nil {
  58  			errorMsg = err.Error()
  59  		}
  60  		http.Error(w, errorMsg, http.StatusUnauthorized)
  61  		return
  62  	}
  63  
  64  	// Check permissions - require owner level
  65  	accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
  66  	if accessLevel != "owner" {
  67  		http.Error(w, "Owner permission required", http.StatusForbidden)
  68  		return
  69  	}
  70  
  71  	// Check if event store is available
  72  	if s.nrcEventStore == nil {
  73  		http.Error(w, "NRC not configured", http.StatusServiceUnavailable)
  74  		return
  75  	}
  76  
  77  	// Get all connections
  78  	conns, err := s.nrcEventStore.GetAllNRCConnections()
  79  	if chk.E(err) {
  80  		http.Error(w, "Failed to get connections", http.StatusInternalServerError)
  81  		return
  82  	}
  83  
  84  	// Get relay identity for config
  85  	relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
  86  	if chk.E(err) {
  87  		http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
  88  		return
  89  	}
  90  	relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
  91  
  92  	// Get NRC config values
  93  	nrcEnabled, nrcRendezvousURL, _, _ := s.Config.GetNRCConfigValues()
  94  
  95  	// Build response
  96  	response := NRCConnectionsResponse{
  97  		Connections: make([]NRCConnectionResponse, 0, len(conns)),
  98  		Config: NRCConfigResponse{
  99  			Enabled:       nrcEnabled,
 100  			RendezvousURL: nrcRendezvousURL,
 101  			RelayPubkey:   string(hex.Enc(relayPubkey)),
 102  		},
 103  	}
 104  
 105  	for _, conn := range conns {
 106  		response.Connections = append(response.Connections, NRCConnectionResponse{
 107  			ID:            conn.ID,
 108  			Label:         conn.Label,
 109  			RendezvousURL: conn.RendezvousURL,
 110  			CreatedAt:     conn.CreatedAt,
 111  			LastUsed:      conn.LastUsed,
 112  		})
 113  	}
 114  
 115  	w.Header().Set("Content-Type", "application/json")
 116  	json.NewEncoder(w).Encode(response)
 117  }
 118  
 119  // handleNRCCreate handles POST /api/nrc/connections
 120  func (s *Server) handleNRCCreate(w http.ResponseWriter, r *http.Request) {
 121  	if r.Method != http.MethodPost {
 122  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 123  		return
 124  	}
 125  
 126  	// Validate NIP-98 authentication
 127  	valid, pubkey, err := httpauth.CheckAuth(r)
 128  	if chk.E(err) || !valid {
 129  		errorMsg := "NIP-98 authentication validation failed"
 130  		if err != nil {
 131  			errorMsg = err.Error()
 132  		}
 133  		http.Error(w, errorMsg, http.StatusUnauthorized)
 134  		return
 135  	}
 136  
 137  	// Check permissions - require owner level
 138  	accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
 139  	if accessLevel != "owner" {
 140  		http.Error(w, "Owner permission required", http.StatusForbidden)
 141  		return
 142  	}
 143  
 144  	// Check if event store is available
 145  	if s.nrcEventStore == nil {
 146  		http.Error(w, "NRC not configured", http.StatusServiceUnavailable)
 147  		return
 148  	}
 149  
 150  	// Parse request body
 151  	var req NRCCreateRequest
 152  	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 153  		http.Error(w, "Invalid request body", http.StatusBadRequest)
 154  		return
 155  	}
 156  
 157  	// Validate label
 158  	req.Label = strings.TrimSpace(req.Label)
 159  	if req.Label == "" {
 160  		http.Error(w, "Label is required", http.StatusBadRequest)
 161  		return
 162  	}
 163  
 164  	// Validate rendezvous URL
 165  	req.RendezvousURL = strings.TrimSpace(req.RendezvousURL)
 166  	if req.RendezvousURL == "" {
 167  		http.Error(w, "Rendezvous URL is required", http.StatusBadRequest)
 168  		return
 169  	}
 170  
 171  	// Create the connection (pass the creator's pubkey for tracking)
 172  	conn, err := s.nrcEventStore.CreateNRCConnection(req.Label, req.RendezvousURL, pubkey)
 173  	if chk.E(err) {
 174  		http.Error(w, "Failed to create connection", http.StatusInternalServerError)
 175  		return
 176  	}
 177  
 178  	// Get relay identity for URI generation
 179  	relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
 180  	if chk.E(err) {
 181  		http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
 182  		return
 183  	}
 184  	relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
 185  
 186  	// Generate URI (uses rendezvous URL stored in connection)
 187  	uri, err := s.nrcEventStore.GetNRCConnectionURI(conn, relayPubkey)
 188  	if chk.E(err) {
 189  		log.W.F("failed to generate URI for new connection: %v", err)
 190  	}
 191  
 192  	// Update bridge authorized secrets if bridge is running
 193  	s.updateNRCBridgeSecretsFromEventStore()
 194  
 195  	// Build response with URI
 196  	response := NRCConnectionResponse{
 197  		ID:            conn.ID,
 198  		Label:         conn.Label,
 199  		RendezvousURL: conn.RendezvousURL,
 200  		CreatedAt:     conn.CreatedAt,
 201  		LastUsed:      conn.LastUsed,
 202  		URI:           uri,
 203  	}
 204  
 205  	w.Header().Set("Content-Type", "application/json")
 206  	w.WriteHeader(http.StatusCreated)
 207  	json.NewEncoder(w).Encode(response)
 208  }
 209  
 210  // handleNRCDelete handles DELETE /api/nrc/connections/{id}
 211  func (s *Server) handleNRCDelete(w http.ResponseWriter, r *http.Request) {
 212  	if r.Method != http.MethodDelete {
 213  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 214  		return
 215  	}
 216  
 217  	// Validate NIP-98 authentication
 218  	valid, pubkey, err := httpauth.CheckAuth(r)
 219  	if chk.E(err) || !valid {
 220  		errorMsg := "NIP-98 authentication validation failed"
 221  		if err != nil {
 222  			errorMsg = err.Error()
 223  		}
 224  		http.Error(w, errorMsg, http.StatusUnauthorized)
 225  		return
 226  	}
 227  
 228  	// Check permissions - require owner level
 229  	accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
 230  	if accessLevel != "owner" {
 231  		http.Error(w, "Owner permission required", http.StatusForbidden)
 232  		return
 233  	}
 234  
 235  	// Check if event store is available
 236  	if s.nrcEventStore == nil {
 237  		http.Error(w, "NRC not configured", http.StatusServiceUnavailable)
 238  		return
 239  	}
 240  
 241  	// Extract connection ID from URL path
 242  	// URL format: /api/nrc/connections/{id}
 243  	path := strings.TrimPrefix(r.URL.Path, "/api/nrc/connections/")
 244  	connID := strings.TrimSpace(path)
 245  	if connID == "" {
 246  		http.Error(w, "Connection ID required", http.StatusBadRequest)
 247  		return
 248  	}
 249  
 250  	// Delete the connection
 251  	if err := s.nrcEventStore.DeleteNRCConnection(connID); chk.E(err) {
 252  		http.Error(w, "Failed to delete connection", http.StatusInternalServerError)
 253  		return
 254  	}
 255  
 256  	// Update bridge authorized secrets if bridge is running
 257  	s.updateNRCBridgeSecretsFromEventStore()
 258  
 259  	log.I.F("deleted NRC connection: %s", connID)
 260  
 261  	w.Header().Set("Content-Type", "application/json")
 262  	json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
 263  }
 264  
 265  // handleNRCGetURI handles GET /api/nrc/connections/{id}/uri
 266  func (s *Server) handleNRCGetURI(w http.ResponseWriter, r *http.Request) {
 267  	if r.Method != http.MethodGet {
 268  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 269  		return
 270  	}
 271  
 272  	// Validate NIP-98 authentication
 273  	valid, pubkey, err := httpauth.CheckAuth(r)
 274  	if chk.E(err) || !valid {
 275  		errorMsg := "NIP-98 authentication validation failed"
 276  		if err != nil {
 277  			errorMsg = err.Error()
 278  		}
 279  		http.Error(w, errorMsg, http.StatusUnauthorized)
 280  		return
 281  	}
 282  
 283  	// Check permissions - require owner level
 284  	accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
 285  	if accessLevel != "owner" {
 286  		http.Error(w, "Owner permission required", http.StatusForbidden)
 287  		return
 288  	}
 289  
 290  	// Check if event store is available
 291  	if s.nrcEventStore == nil {
 292  		http.Error(w, "NRC not configured", http.StatusServiceUnavailable)
 293  		return
 294  	}
 295  
 296  	// Extract connection ID from URL path
 297  	// URL format: /api/nrc/connections/{id}/uri
 298  	path := strings.TrimPrefix(r.URL.Path, "/api/nrc/connections/")
 299  	path = strings.TrimSuffix(path, "/uri")
 300  	connID := strings.TrimSpace(path)
 301  	if connID == "" {
 302  		http.Error(w, "Connection ID required", http.StatusBadRequest)
 303  		return
 304  	}
 305  
 306  	// Get the connection
 307  	conn, err := s.nrcEventStore.GetNRCConnection(connID)
 308  	if err != nil {
 309  		http.Error(w, "Connection not found", http.StatusNotFound)
 310  		return
 311  	}
 312  
 313  	// Get relay identity
 314  	relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
 315  	if chk.E(err) {
 316  		http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
 317  		return
 318  	}
 319  	relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
 320  
 321  	// Generate URI (uses rendezvous URL stored in connection)
 322  	uri, err := s.nrcEventStore.GetNRCConnectionURI(conn, relayPubkey)
 323  	if chk.E(err) {
 324  		http.Error(w, "Failed to generate URI", http.StatusInternalServerError)
 325  		return
 326  	}
 327  
 328  	w.Header().Set("Content-Type", "application/json")
 329  	json.NewEncoder(w).Encode(map[string]string{"uri": uri})
 330  }
 331  
 332  // updateNRCBridgeSecretsFromEventStore updates the NRC bridge with current authorized secrets from event store.
 333  func (s *Server) updateNRCBridgeSecretsFromEventStore() {
 334  	if s.nrcBridge == nil || s.nrcEventStore == nil {
 335  		return
 336  	}
 337  
 338  	secrets, err := s.nrcEventStore.GetNRCAuthorizedSecrets()
 339  	if chk.E(err) {
 340  		log.W.F("failed to get NRC authorized secrets: %v", err)
 341  		return
 342  	}
 343  
 344  	s.nrcBridge.UpdateAuthorizedSecrets(secrets)
 345  	log.D.F("updated NRC bridge with %d authorized secrets from event store", len(secrets))
 346  }
 347  
 348  // handleNRCConnectionsRouter routes NRC connection requests.
 349  func (s *Server) handleNRCConnectionsRouter(w http.ResponseWriter, r *http.Request) {
 350  	path := r.URL.Path
 351  
 352  	// Exact match for /api/nrc/connections
 353  	if path == "/api/nrc/connections" {
 354  		switch r.Method {
 355  		case http.MethodGet:
 356  			s.handleNRCConnections(w, r)
 357  		case http.MethodPost:
 358  			s.handleNRCCreate(w, r)
 359  		default:
 360  			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 361  		}
 362  		return
 363  	}
 364  
 365  	// Check for /api/nrc/connections/{id}/uri
 366  	if strings.HasSuffix(path, "/uri") {
 367  		s.handleNRCGetURI(w, r)
 368  		return
 369  	}
 370  
 371  	// Otherwise it's /api/nrc/connections/{id}
 372  	s.handleNRCDelete(w, r)
 373  }
 374  
 375  // handleNRCConfig returns NRC configuration status.
 376  func (s *Server) handleNRCConfig(w http.ResponseWriter, r *http.Request) {
 377  	if r.Method != http.MethodGet {
 378  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 379  		return
 380  	}
 381  
 382  	// Get NRC config values
 383  	nrcEnabled, nrcRendezvousURL, _, _ := s.Config.GetNRCConfigValues()
 384  
 385  	// Check if NRC bridge is actually running
 386  	bridgeRunning := s.nrcBridge != nil
 387  
 388  	// Check if event store is available for connection management
 389  	eventStoreAvailable := s.nrcEventStore != nil
 390  
 391  	response := struct {
 392  		Enabled           bool   `json:"enabled"`
 393  		ConnectionMgmtOK  bool   `json:"connection_mgmt_ok"`
 394  		RendezvousURL     string `json:"rendezvous_url,omitempty"`
 395  	}{
 396  		Enabled:          nrcEnabled && bridgeRunning,
 397  		ConnectionMgmtOK: eventStoreAvailable,
 398  		RendezvousURL:    nrcRendezvousURL,
 399  	}
 400  
 401  	w.Header().Set("Content-Type", "application/json")
 402  	json.NewEncoder(w).Encode(response)
 403  }
 404