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