nrc_events.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"context"
   7  	"crypto/rand"
   8  	"encoding/json"
   9  	"fmt"
  10  	"time"
  11  
  12  	"next.orly.dev/pkg/lol/chk"
  13  	"next.orly.dev/pkg/lol/log"
  14  
  15  	"next.orly.dev/pkg/nostr/crypto/keys"
  16  	"next.orly.dev/pkg/nostr/encoders/event"
  17  	"next.orly.dev/pkg/nostr/encoders/filter"
  18  	"next.orly.dev/pkg/nostr/encoders/hex"
  19  	"next.orly.dev/pkg/nostr/encoders/kind"
  20  	"next.orly.dev/pkg/nostr/encoders/tag"
  21  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  22  )
  23  
  24  // NRC connection event kind - using application-specific data range
  25  // Kind 30078 is commonly used for app-specific data
  26  const KindNRCConnection = uint16(30078)
  27  
  28  // NRCEventStore provides NRC connection management using events.
  29  // This works with any Database implementation that supports SaveEvent/QueryEvents.
  30  type NRCEventStore struct {
  31  	db          Database
  32  	relaySigner *p8k.Signer
  33  	relayPubkey []byte
  34  }
  35  
  36  // NewNRCEventStore creates a new event-based NRC store.
  37  // relaySigner is used to sign NRC connection events.
  38  func NewNRCEventStore(db Database, relaySigner *p8k.Signer) *NRCEventStore {
  39  	return &NRCEventStore{
  40  		db:          db,
  41  		relaySigner: relaySigner,
  42  		relayPubkey: relaySigner.Pub(),
  43  	}
  44  }
  45  
  46  // nrcConnectionContent is the JSON structure stored in event content
  47  type nrcConnectionContent struct {
  48  	ID            string `json:"id"`
  49  	Label         string `json:"label"`
  50  	Secret        string `json:"secret"`         // hex encoded
  51  	DerivedPubkey string `json:"derived_pubkey"` // hex encoded
  52  	RendezvousURL string `json:"rendezvous_url"` // WebSocket URL of rendezvous relay
  53  	CreatedAt     int64  `json:"created_at"`
  54  	LastUsed      int64  `json:"last_used"`
  55  	CreatedBy     string `json:"created_by"` // hex encoded
  56  }
  57  
  58  // CreateNRCConnection generates a new NRC connection with a random secret.
  59  func (s *NRCEventStore) CreateNRCConnection(label string, rendezvousURL string, createdBy []byte) (*NRCConnection, error) {
  60  	// Generate random 32-byte secret
  61  	secret := make([]byte, 32)
  62  	if _, err := rand.Read(secret); err != nil {
  63  		return nil, fmt.Errorf("failed to generate random secret: %w", err)
  64  	}
  65  
  66  	// Derive pubkey from secret
  67  	derivedPubkey, err := keys.SecretBytesToPubKeyBytes(secret)
  68  	if err != nil {
  69  		return nil, fmt.Errorf("failed to derive pubkey from secret: %w", err)
  70  	}
  71  
  72  	// Use first 8 bytes of secret as ID (hex encoded = 16 chars)
  73  	id := string(hex.Enc(secret[:8]))
  74  
  75  	conn := &NRCConnection{
  76  		ID:            id,
  77  		Label:         label,
  78  		Secret:        secret,
  79  		DerivedPubkey: derivedPubkey,
  80  		RendezvousURL: rendezvousURL,
  81  		CreatedAt:     time.Now().Unix(),
  82  		LastUsed:      0,
  83  		CreatedBy:     createdBy,
  84  	}
  85  
  86  	if err := s.SaveNRCConnection(conn); chk.E(err) {
  87  		return nil, err
  88  	}
  89  
  90  	log.I.F("created NRC connection: id=%s label=%s rendezvous=%s", id, label, rendezvousURL)
  91  	return conn, nil
  92  }
  93  
  94  // SaveNRCConnection stores an NRC connection as an event.
  95  func (s *NRCEventStore) SaveNRCConnection(conn *NRCConnection) error {
  96  	// Create content JSON
  97  	content := nrcConnectionContent{
  98  		ID:            conn.ID,
  99  		Label:         conn.Label,
 100  		Secret:        string(hex.Enc(conn.Secret)),
 101  		DerivedPubkey: string(hex.Enc(conn.DerivedPubkey)),
 102  		RendezvousURL: conn.RendezvousURL,
 103  		CreatedAt:     conn.CreatedAt,
 104  		LastUsed:      conn.LastUsed,
 105  	}
 106  	if len(conn.CreatedBy) > 0 {
 107  		content.CreatedBy = string(hex.Enc(conn.CreatedBy))
 108  	}
 109  
 110  	contentJSON, err := json.Marshal(content)
 111  	if err != nil {
 112  		return fmt.Errorf("failed to marshal NRC connection: %w", err)
 113  	}
 114  
 115  	// Create replaceable event with d-tag = connection ID
 116  	// Use "p" tag for derived_pubkey since only single-letter tags are indexed
 117  	ev := &event.E{
 118  		Kind:      KindNRCConnection,
 119  		CreatedAt: time.Now().Unix(),
 120  		Tags: tag.NewS(
 121  			tag.NewFromAny("d", conn.ID),
 122  			tag.NewFromAny("p", string(hex.Enc(conn.DerivedPubkey))), // derived pubkey for authorization lookup
 123  		),
 124  		Content: contentJSON,
 125  	}
 126  
 127  	// Sign with relay identity
 128  	if err := ev.Sign(s.relaySigner); err != nil {
 129  		return fmt.Errorf("failed to sign NRC connection event: %w", err)
 130  	}
 131  
 132  	// Save to database
 133  	ctx := context.Background()
 134  	if _, err := s.db.SaveEvent(ctx, ev); err != nil {
 135  		return fmt.Errorf("failed to save NRC connection event: %w", err)
 136  	}
 137  
 138  	return nil
 139  }
 140  
 141  // GetNRCConnection retrieves an NRC connection by ID.
 142  func (s *NRCEventStore) GetNRCConnection(id string) (*NRCConnection, error) {
 143  	ctx := context.Background()
 144  
 145  	// Query for the specific connection event
 146  	limit := uint(1)
 147  	f := &filter.F{
 148  		Kinds:   kind.NewS(kind.New(KindNRCConnection)),
 149  		Authors: tag.NewFromBytesSlice(s.relayPubkey),
 150  		Tags:    tag.NewS(tag.NewFromAny("d", id)),
 151  		Limit:   &limit,
 152  	}
 153  
 154  	events, err := s.db.QueryEvents(ctx, f)
 155  	if err != nil {
 156  		return nil, err
 157  	}
 158  	if len(events) == 0 {
 159  		return nil, fmt.Errorf("NRC connection not found: %s", id)
 160  	}
 161  
 162  	return s.eventToConnection(events[0])
 163  }
 164  
 165  // GetNRCConnectionByDerivedPubkey retrieves an NRC connection by its derived pubkey.
 166  func (s *NRCEventStore) GetNRCConnectionByDerivedPubkey(derivedPubkey []byte) (*NRCConnection, error) {
 167  	ctx := context.Background()
 168  	pubkeyHex := string(hex.Enc(derivedPubkey))
 169  
 170  	// Query by "p" tag (derived pubkey) - single-letter tags are indexed
 171  	limit := uint(1)
 172  	f := &filter.F{
 173  		Kinds:   kind.NewS(kind.New(KindNRCConnection)),
 174  		Authors: tag.NewFromBytesSlice(s.relayPubkey),
 175  		Tags:    tag.NewS(tag.NewFromAny("p", pubkeyHex)),
 176  		Limit:   &limit,
 177  	}
 178  
 179  	events, err := s.db.QueryEvents(ctx, f)
 180  	if err != nil {
 181  		return nil, err
 182  	}
 183  	if len(events) == 0 {
 184  		return nil, fmt.Errorf("NRC connection not found for pubkey")
 185  	}
 186  
 187  	return s.eventToConnection(events[0])
 188  }
 189  
 190  // DeleteNRCConnection removes an NRC connection by deleting its event.
 191  func (s *NRCEventStore) DeleteNRCConnection(id string) error {
 192  	ctx := context.Background()
 193  
 194  	// Query to find the event
 195  	limit := uint(1)
 196  	f := &filter.F{
 197  		Kinds:   kind.NewS(kind.New(KindNRCConnection)),
 198  		Authors: tag.NewFromBytesSlice(s.relayPubkey),
 199  		Tags:    tag.NewS(tag.NewFromAny("d", id)),
 200  		Limit:   &limit,
 201  	}
 202  
 203  	events, err := s.db.QueryEvents(ctx, f)
 204  	if err != nil {
 205  		return err
 206  	}
 207  	if len(events) == 0 {
 208  		return nil // Already deleted
 209  	}
 210  
 211  	// Delete the event
 212  	return s.db.DeleteEvent(ctx, events[0].ID)
 213  }
 214  
 215  // GetAllNRCConnections returns all NRC connections.
 216  func (s *NRCEventStore) GetAllNRCConnections() ([]*NRCConnection, error) {
 217  	ctx := context.Background()
 218  
 219  	// Query all NRC connection events from this relay
 220  	f := &filter.F{
 221  		Kinds:   kind.NewS(kind.New(KindNRCConnection)),
 222  		Authors: tag.NewFromBytesSlice(s.relayPubkey),
 223  	}
 224  
 225  	events, err := s.db.QueryEvents(ctx, f)
 226  	if err != nil {
 227  		return nil, err
 228  	}
 229  
 230  	conns := make([]*NRCConnection, 0, len(events))
 231  	for _, ev := range events {
 232  		conn, err := s.eventToConnection(ev)
 233  		if err != nil {
 234  			log.W.F("failed to parse NRC connection event: %v", err)
 235  			continue
 236  		}
 237  		conns = append(conns, conn)
 238  	}
 239  
 240  	return conns, nil
 241  }
 242  
 243  // GetNRCAuthorizedSecrets returns a map of derived pubkeys to labels.
 244  func (s *NRCEventStore) GetNRCAuthorizedSecrets() (map[string]string, error) {
 245  	conns, err := s.GetAllNRCConnections()
 246  	if err != nil {
 247  		return nil, err
 248  	}
 249  
 250  	result := make(map[string]string)
 251  	for _, conn := range conns {
 252  		pubkeyHex := string(hex.Enc(conn.DerivedPubkey))
 253  		result[pubkeyHex] = conn.Label
 254  	}
 255  
 256  	return result, nil
 257  }
 258  
 259  // UpdateNRCConnectionLastUsed updates the last used timestamp.
 260  func (s *NRCEventStore) UpdateNRCConnectionLastUsed(id string) error {
 261  	conn, err := s.GetNRCConnection(id)
 262  	if err != nil {
 263  		return err
 264  	}
 265  
 266  	conn.LastUsed = time.Now().Unix()
 267  	return s.SaveNRCConnection(conn)
 268  }
 269  
 270  // GetNRCConnectionURI generates the full connection URI for a connection.
 271  // Uses the rendezvous URL stored in the connection.
 272  func (s *NRCEventStore) GetNRCConnectionURI(conn *NRCConnection, relayPubkey []byte) (string, error) {
 273  	if len(relayPubkey) != 32 {
 274  		return "", fmt.Errorf("invalid relay pubkey length: %d", len(relayPubkey))
 275  	}
 276  	if conn.RendezvousURL == "" {
 277  		return "", fmt.Errorf("connection has no rendezvous URL")
 278  	}
 279  
 280  	relayPubkeyHex := hex.Enc(relayPubkey)
 281  	secretHex := hex.Enc(conn.Secret)
 282  
 283  	uri := fmt.Sprintf("nostr+relayconnect://%s?relay=%s&secret=%s",
 284  		relayPubkeyHex, conn.RendezvousURL, secretHex)
 285  
 286  	if conn.Label != "" {
 287  		uri += fmt.Sprintf("&name=%s", conn.Label)
 288  	}
 289  
 290  	return uri, nil
 291  }
 292  
 293  // eventToConnection parses an event into an NRCConnection.
 294  func (s *NRCEventStore) eventToConnection(ev *event.E) (*NRCConnection, error) {
 295  	var content nrcConnectionContent
 296  	if err := json.Unmarshal(ev.Content, &content); err != nil {
 297  		return nil, fmt.Errorf("failed to unmarshal NRC connection content: %w", err)
 298  	}
 299  
 300  	secret, err := hex.Dec(content.Secret)
 301  	if err != nil {
 302  		return nil, fmt.Errorf("failed to decode secret: %w", err)
 303  	}
 304  
 305  	derivedPubkey, err := hex.Dec(content.DerivedPubkey)
 306  	if err != nil {
 307  		return nil, fmt.Errorf("failed to decode derived pubkey: %w", err)
 308  	}
 309  
 310  	var createdBy []byte
 311  	if content.CreatedBy != "" {
 312  		createdBy, _ = hex.Dec(content.CreatedBy)
 313  	}
 314  
 315  	return &NRCConnection{
 316  		ID:            content.ID,
 317  		Label:         content.Label,
 318  		Secret:        secret,
 319  		DerivedPubkey: derivedPubkey,
 320  		RendezvousURL: content.RendezvousURL,
 321  		CreatedAt:     content.CreatedAt,
 322  		LastUsed:      content.LastUsed,
 323  		CreatedBy:     createdBy,
 324  	}, nil
 325  }
 326  
 327  // NRCEventAuthorizer wraps NRCEventStore to implement the NRC authorization interface.
 328  type NRCEventAuthorizer struct {
 329  	store *NRCEventStore
 330  }
 331  
 332  // NewNRCEventAuthorizer creates a new NRC authorizer from an event store.
 333  func NewNRCEventAuthorizer(store *NRCEventStore) *NRCEventAuthorizer {
 334  	return &NRCEventAuthorizer{store: store}
 335  }
 336  
 337  // GetNRCClientByPubkey looks up an authorized client by their derived pubkey.
 338  func (a *NRCEventAuthorizer) GetNRCClientByPubkey(derivedPubkey []byte) (id string, label string, found bool, err error) {
 339  	conn, err := a.store.GetNRCConnectionByDerivedPubkey(derivedPubkey)
 340  	if err != nil {
 341  		// Not found is not an error, just means not authorized
 342  		return "", "", false, nil
 343  	}
 344  	if conn == nil {
 345  		return "", "", false, nil
 346  	}
 347  	return conn.ID, conn.Label, true, nil
 348  }
 349  
 350  // UpdateNRCClientLastUsed updates the last used timestamp for tracking.
 351  func (a *NRCEventAuthorizer) UpdateNRCClientLastUsed(id string) error {
 352  	return a.store.UpdateNRCConnectionLastUsed(id)
 353  }
 354