Files
next.orly.dev/app/handle-nrc.go
woikos 6a38779794
Some checks are pending
Go / build-and-release (push) Waiting to run
Remove Cashu Access Token (CAT) system entirely (v0.52.3)
- Delete pkg/cashu/ package (BDHKE, issuer, verifier, keyset, token)
- Delete pkg/interfaces/cashu/ interface definitions
- Delete pkg/bunker/acl_adapter.go CAT authorization checker
- Delete app/handle-cashu.go HTTP handlers for mint endpoints
- Delete docs/NIP-XX-CASHU-ACCESS-TOKENS.md specification
- Remove Cashu config fields from app/config/config.go
- Remove CashuIssuer/CashuVerifier from app/server.go
- Remove CAT initialization and NRC Cashu verifier from app/main.go
- Remove token extraction from app/handle-websocket.go
- Remove CAT permission checks from app/handle-event.go
- Remove CashuEnabled from bunker info response
- Remove UseCashu field from NRC connections
- Remove AuthModeCAT from NRC protocol
- Remove CAT UI from BunkerView.svelte and RelayConnectView.svelte
- Remove cashu-client.js from web UI
- Add missing bunker worker stores to stores.js

Files modified:
- app/config/config.go: Removed Cashu config fields
- app/server.go: Removed Cashu issuer/verifier
- app/main.go: Removed Cashu initialization
- app/handle-*.go: Removed CAT checks and handlers
- app/listener.go: Removed cashuToken field
- pkg/database/nrc.go: Removed UseCashu field
- pkg/protocol/nrc/: Removed CAT auth mode and handling
- pkg/event/authorization/: Removed CAT import
- app/web/src/: Removed CAT UI components and logic
- main.go: Removed CAT help text

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 05:29:21 +01:00

401 lines
11 KiB
Go

package app
import (
"encoding/json"
"net/http"
"strings"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/crypto/keys"
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/httpauth"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/database"
)
// NRCConnectionResponse is the response structure for NRC connection API.
type NRCConnectionResponse struct {
ID string `json:"id"`
Label string `json:"label"`
CreatedAt int64 `json:"created_at"`
LastUsed int64 `json:"last_used"`
URI string `json:"uri,omitempty"` // Only included when specifically requested
}
// NRCConnectionsResponse is the response for listing all connections.
type NRCConnectionsResponse struct {
Connections []NRCConnectionResponse `json:"connections"`
Config NRCConfigResponse `json:"config"`
}
// NRCConfigResponse contains NRC configuration status.
type NRCConfigResponse struct {
Enabled bool `json:"enabled"`
RendezvousURL string `json:"rendezvous_url"`
RelayPubkey string `json:"relay_pubkey"`
}
// NRCCreateRequest is the request body for creating a connection.
type NRCCreateRequest struct {
Label string `json:"label"`
}
// handleNRCConnections handles GET /api/nrc/connections
func (s *Server) handleNRCConnections(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Get all connections
conns, err := badgerDB.GetAllNRCConnections()
if chk.E(err) {
http.Error(w, "Failed to get connections", http.StatusInternalServerError)
return
}
// Get relay identity for config
relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
if chk.E(err) {
http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
return
}
relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
// Get NRC config values
nrcEnabled, nrcRendezvousURL, _, _ := s.Config.GetNRCConfigValues()
// Build response
response := NRCConnectionsResponse{
Connections: make([]NRCConnectionResponse, 0, len(conns)),
Config: NRCConfigResponse{
Enabled: nrcEnabled,
RendezvousURL: nrcRendezvousURL,
RelayPubkey: string(hex.Enc(relayPubkey)),
},
}
for _, conn := range conns {
response.Connections = append(response.Connections, NRCConnectionResponse{
ID: conn.ID,
Label: conn.Label,
CreatedAt: conn.CreatedAt,
LastUsed: conn.LastUsed,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// handleNRCCreate handles POST /api/nrc/connections
func (s *Server) handleNRCCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Parse request body
var req NRCCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Validate label
req.Label = strings.TrimSpace(req.Label)
if req.Label == "" {
http.Error(w, "Label is required", http.StatusBadRequest)
return
}
// Create the connection
conn, err := badgerDB.CreateNRCConnection(req.Label)
if chk.E(err) {
http.Error(w, "Failed to create connection", http.StatusInternalServerError)
return
}
// Get relay identity for URI generation
relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
if chk.E(err) {
http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
return
}
relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
// Get NRC config values
_, nrcRendezvousURL, _, _ := s.Config.GetNRCConfigValues()
// Generate URI
uri, err := badgerDB.GetNRCConnectionURI(conn, relayPubkey, nrcRendezvousURL)
if chk.E(err) {
log.W.F("failed to generate URI for new connection: %v", err)
}
// Update bridge authorized secrets if bridge is running
s.updateNRCBridgeSecrets(badgerDB)
// Build response with URI
response := NRCConnectionResponse{
ID: conn.ID,
Label: conn.Label,
CreatedAt: conn.CreatedAt,
LastUsed: conn.LastUsed,
URI: uri,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
}
// handleNRCDelete handles DELETE /api/nrc/connections/{id}
func (s *Server) handleNRCDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Extract connection ID from URL path
// URL format: /api/nrc/connections/{id}
path := strings.TrimPrefix(r.URL.Path, "/api/nrc/connections/")
connID := strings.TrimSpace(path)
if connID == "" {
http.Error(w, "Connection ID required", http.StatusBadRequest)
return
}
// Delete the connection
if err := badgerDB.DeleteNRCConnection(connID); chk.E(err) {
http.Error(w, "Failed to delete connection", http.StatusInternalServerError)
return
}
// Update bridge authorized secrets if bridge is running
s.updateNRCBridgeSecrets(badgerDB)
log.I.F("deleted NRC connection: %s", connID)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// handleNRCGetURI handles GET /api/nrc/connections/{id}/uri
func (s *Server) handleNRCGetURI(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Extract connection ID from URL path
// URL format: /api/nrc/connections/{id}/uri
path := strings.TrimPrefix(r.URL.Path, "/api/nrc/connections/")
path = strings.TrimSuffix(path, "/uri")
connID := strings.TrimSpace(path)
if connID == "" {
http.Error(w, "Connection ID required", http.StatusBadRequest)
return
}
// Get the connection
conn, err := badgerDB.GetNRCConnection(connID)
if err != nil {
http.Error(w, "Connection not found", http.StatusNotFound)
return
}
// Get relay identity
relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
if chk.E(err) {
http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
return
}
relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
// Get NRC config values
_, nrcRendezvousURL, _, _ := s.Config.GetNRCConfigValues()
// Generate URI
uri, err := badgerDB.GetNRCConnectionURI(conn, relayPubkey, nrcRendezvousURL)
if chk.E(err) {
http.Error(w, "Failed to generate URI", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"uri": uri})
}
// updateNRCBridgeSecrets updates the NRC bridge with current authorized secrets from database.
func (s *Server) updateNRCBridgeSecrets(badgerDB *database.D) {
if s.nrcBridge == nil {
return
}
secrets, err := badgerDB.GetNRCAuthorizedSecrets()
if chk.E(err) {
log.W.F("failed to get NRC authorized secrets: %v", err)
return
}
s.nrcBridge.UpdateAuthorizedSecrets(secrets)
log.D.F("updated NRC bridge with %d authorized secrets", len(secrets))
}
// handleNRCConnectionsRouter routes NRC connection requests.
func (s *Server) handleNRCConnectionsRouter(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Exact match for /api/nrc/connections
if path == "/api/nrc/connections" {
switch r.Method {
case http.MethodGet:
s.handleNRCConnections(w, r)
case http.MethodPost:
s.handleNRCCreate(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
return
}
// Check for /api/nrc/connections/{id}/uri
if strings.HasSuffix(path, "/uri") {
s.handleNRCGetURI(w, r)
return
}
// Otherwise it's /api/nrc/connections/{id}
s.handleNRCDelete(w, r)
}
// handleNRCConfig returns NRC configuration status.
func (s *Server) handleNRCConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Get NRC config values
nrcEnabled, nrcRendezvousURL, _, _ := s.Config.GetNRCConfigValues()
// Check if Badger is available (NRC requires Badger)
_, badgerAvailable := s.DB.(*database.D)
response := struct {
Enabled bool `json:"enabled"`
BadgerRequired bool `json:"badger_required"`
RendezvousURL string `json:"rendezvous_url,omitempty"`
}{
Enabled: nrcEnabled && badgerAvailable,
BadgerRequired: !badgerAvailable,
RendezvousURL: nrcRendezvousURL,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}