You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

206 lines
5.6 KiB

//go:build !(js && wasm)
package database
import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/dgraph-io/badger/v4"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/crypto/keys"
"git.mleku.dev/mleku/nostr/encoders/hex"
)
// Key prefixes for NRC data
const (
nrcConnectionPrefix = "nrc:conn:" // NRC connections by ID
)
// NRCConnection stores an NRC connection configuration in the database.
type NRCConnection struct {
ID string `json:"id"` // Unique identifier (hex of first 8 bytes of secret)
Label string `json:"label"` // Human-readable label (e.g., "Phone", "Laptop")
Secret []byte `json:"secret"` // 32-byte secret for client authentication
CreatedAt int64 `json:"created_at"` // Unix timestamp
LastUsed int64 `json:"last_used"` // Unix timestamp of last connection (0 if never)
UseCashu bool `json:"use_cashu"` // Whether to include CAT token in URI
}
// GetNRCConnection retrieves an NRC connection by ID.
func (d *D) GetNRCConnection(id string) (conn *NRCConnection, err error) {
key := []byte(nrcConnectionPrefix + id)
err = d.DB.View(func(txn *badger.Txn) error {
item, err := txn.Get(key)
if errors.Is(err, badger.ErrKeyNotFound) {
return err
}
if err != nil {
return err
}
return item.Value(func(val []byte) error {
conn = &NRCConnection{}
return json.Unmarshal(val, conn)
})
})
return
}
// SaveNRCConnection stores an NRC connection in the database.
func (d *D) SaveNRCConnection(conn *NRCConnection) error {
data, err := json.Marshal(conn)
if err != nil {
return fmt.Errorf("failed to marshal connection: %w", err)
}
key := []byte(nrcConnectionPrefix + conn.ID)
return d.DB.Update(func(txn *badger.Txn) error {
return txn.Set(key, data)
})
}
// DeleteNRCConnection removes an NRC connection from the database.
func (d *D) DeleteNRCConnection(id string) error {
key := []byte(nrcConnectionPrefix + id)
return d.DB.Update(func(txn *badger.Txn) error {
if err := txn.Delete(key); err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
return err
}
return nil
})
}
// GetAllNRCConnections returns all NRC connections.
func (d *D) GetAllNRCConnections() (conns []*NRCConnection, err error) {
prefix := []byte(nrcConnectionPrefix)
err = d.DB.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
opts.Prefix = prefix
it := txn.NewIterator(opts)
defer it.Close()
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
item := it.Item()
err := item.Value(func(val []byte) error {
conn := &NRCConnection{}
if err := json.Unmarshal(val, conn); err != nil {
return err
}
conns = append(conns, conn)
return nil
})
if err != nil {
return err
}
}
return nil
})
return
}
// CreateNRCConnection generates a new NRC connection with a random secret.
func (d *D) CreateNRCConnection(label string, useCashu bool) (*NRCConnection, error) {
// Generate random 32-byte secret
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("failed to generate random secret: %w", err)
}
// Use first 8 bytes of secret as ID (hex encoded = 16 chars)
id := string(hex.Enc(secret[:8]))
conn := &NRCConnection{
ID: id,
Label: label,
Secret: secret,
CreatedAt: time.Now().Unix(),
LastUsed: 0,
UseCashu: useCashu,
}
if err := d.SaveNRCConnection(conn); chk.E(err) {
return nil, err
}
log.I.F("created NRC connection: id=%s label=%s cashu=%v", id, label, useCashu)
return conn, nil
}
// GetNRCConnectionURI generates the full connection URI for a connection.
// relayPubkey is the relay's public key (32 bytes).
// rendezvousURL is the public relay URL.
// mintURL is the CAT mint URL (required if useCashu is true).
func (d *D) GetNRCConnectionURI(conn *NRCConnection, relayPubkey []byte, rendezvousURL, mintURL string) (string, error) {
if len(relayPubkey) != 32 {
return "", fmt.Errorf("invalid relay pubkey length: %d", len(relayPubkey))
}
if rendezvousURL == "" {
return "", fmt.Errorf("rendezvous URL is required")
}
relayPubkeyHex := hex.Enc(relayPubkey)
secretHex := hex.Enc(conn.Secret)
var uri string
if conn.UseCashu {
if mintURL == "" {
return "", fmt.Errorf("mint URL is required for CAT authentication")
}
// CAT-based URI includes both secret (for non-CAT relays) and CAT auth
uri = fmt.Sprintf("nostr+relayconnect://%s?relay=%s&secret=%s&auth=cat&mint=%s",
relayPubkeyHex, rendezvousURL, secretHex, mintURL)
} else {
// Secret-only URI
uri = fmt.Sprintf("nostr+relayconnect://%s?relay=%s&secret=%s",
relayPubkeyHex, rendezvousURL, secretHex)
}
if conn.Label != "" {
uri += fmt.Sprintf("&name=%s", conn.Label)
}
return uri, nil
}
// GetNRCAuthorizedSecrets returns a map of derived pubkeys to labels for all connections.
// This is used by the NRC bridge to authorize incoming connections.
func (d *D) GetNRCAuthorizedSecrets() (map[string]string, error) {
conns, err := d.GetAllNRCConnections()
if err != nil {
return nil, err
}
result := make(map[string]string)
for _, conn := range conns {
// Derive pubkey from secret
pubkey, err := keys.SecretBytesToPubKeyBytes(conn.Secret)
if err != nil {
log.W.F("failed to derive pubkey for NRC connection %s: %v", conn.ID, err)
continue
}
pubkeyHex := string(hex.Enc(pubkey))
result[pubkeyHex] = conn.Label
}
return result, nil
}
// UpdateNRCConnectionLastUsed updates the last used timestamp for a connection.
func (d *D) UpdateNRCConnectionLastUsed(id string) error {
conn, err := d.GetNRCConnection(id)
if err != nil {
return err
}
conn.LastUsed = time.Now().Unix()
return d.SaveNRCConnection(conn)
}