types.go raw

   1  package nip43
   2  
   3  import (
   4  	"crypto/rand"
   5  	"encoding/base64"
   6  	"sync"
   7  	"time"
   8  
   9  	"next.orly.dev/pkg/nostr/encoders/event"
  10  	"next.orly.dev/pkg/nostr/encoders/hex"
  11  	"next.orly.dev/pkg/nostr/encoders/tag"
  12  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  13  )
  14  
  15  // Event kinds defined by NIP-43
  16  const (
  17  	KindMemberList   = 13534 // Membership list published by relay
  18  	KindAddUser      = 8000  // Add user event published by relay
  19  	KindRemoveUser   = 8001  // Remove user event published by relay
  20  	KindJoinRequest  = 28934 // Join request sent by user
  21  	KindInviteReq    = 28935 // Invite request (ephemeral)
  22  	KindLeaveRequest = 28936 // Leave request sent by user
  23  )
  24  
  25  // InviteCode represents a claim/invite code for relay access
  26  type InviteCode struct {
  27  	Code      string
  28  	ExpiresAt time.Time
  29  	UsedBy    []byte // pubkey that used this code, nil if unused
  30  	CreatedAt time.Time
  31  }
  32  
  33  // InviteManager manages invite codes for NIP-43
  34  type InviteManager struct {
  35  	mu     sync.RWMutex
  36  	codes  map[string]*InviteCode
  37  	expiry time.Duration
  38  }
  39  
  40  // NewInviteManager creates a new invite code manager
  41  func NewInviteManager(expiryDuration time.Duration) *InviteManager {
  42  	if expiryDuration == 0 {
  43  		expiryDuration = 24 * time.Hour // Default: 24 hours
  44  	}
  45  	return &InviteManager{
  46  		codes:  make(map[string]*InviteCode),
  47  		expiry: expiryDuration,
  48  	}
  49  }
  50  
  51  // GenerateCode creates a new invite code
  52  func (im *InviteManager) GenerateCode() (code string, err error) {
  53  	// Generate 32 random bytes
  54  	b := make([]byte, 32)
  55  	if _, err = rand.Read(b); err != nil {
  56  		return
  57  	}
  58  	code = base64.URLEncoding.EncodeToString(b)
  59  
  60  	im.mu.Lock()
  61  	defer im.mu.Unlock()
  62  
  63  	im.codes[code] = &InviteCode{
  64  		Code:      code,
  65  		CreatedAt: time.Now(),
  66  		ExpiresAt: time.Now().Add(im.expiry),
  67  	}
  68  
  69  	return code, nil
  70  }
  71  
  72  // ValidateAndConsume validates an invite code and marks it as used by the given pubkey
  73  func (im *InviteManager) ValidateAndConsume(code string, pubkey []byte) (valid bool, reason string) {
  74  	im.mu.Lock()
  75  	defer im.mu.Unlock()
  76  
  77  	invite, exists := im.codes[code]
  78  	if !exists {
  79  		return false, "invalid invite code"
  80  	}
  81  
  82  	if time.Now().After(invite.ExpiresAt) {
  83  		delete(im.codes, code)
  84  		return false, "invite code expired"
  85  	}
  86  
  87  	if invite.UsedBy != nil {
  88  		return false, "invite code already used"
  89  	}
  90  
  91  	// Mark as used
  92  	invite.UsedBy = make([]byte, len(pubkey))
  93  	copy(invite.UsedBy, pubkey)
  94  
  95  	return true, ""
  96  }
  97  
  98  // CleanupExpired removes expired invite codes
  99  func (im *InviteManager) CleanupExpired() {
 100  	im.mu.Lock()
 101  	defer im.mu.Unlock()
 102  
 103  	now := time.Now()
 104  	for code, invite := range im.codes {
 105  		if now.After(invite.ExpiresAt) {
 106  			delete(im.codes, code)
 107  		}
 108  	}
 109  }
 110  
 111  // BuildMemberListEvent creates a kind 13534 membership list event
 112  // relaySecretKey: the relay's identity secret key (32 bytes)
 113  // members: list of member pubkeys (32 bytes each)
 114  func BuildMemberListEvent(relaySecretKey []byte, members [][]byte) (*event.E, error) {
 115  	// Create signer
 116  	signer, err := p8k.New()
 117  	if err != nil {
 118  		return nil, err
 119  	}
 120  	if err = signer.InitSec(relaySecretKey); err != nil {
 121  		return nil, err
 122  	}
 123  
 124  	ev := event.New()
 125  	ev.Kind = KindMemberList
 126  	copy(ev.Pubkey, signer.Pub())
 127  
 128  	// Initialize tags
 129  	ev.Tags = tag.NewS()
 130  
 131  	// Add NIP-70 `-` tag
 132  	ev.Tags.Append(tag.NewFromAny("-"))
 133  
 134  	// Add member tags
 135  	for _, member := range members {
 136  		if len(member) == 32 {
 137  			ev.Tags.Append(tag.NewFromAny("member", hex.Enc(member)))
 138  		}
 139  	}
 140  
 141  	ev.CreatedAt = time.Now().Unix()
 142  	ev.Content = []byte("")
 143  
 144  	// Sign the event
 145  	if err := ev.Sign(signer); err != nil {
 146  		return nil, err
 147  	}
 148  
 149  	return ev, nil
 150  }
 151  
 152  // BuildAddUserEvent creates a kind 8000 add user event
 153  func BuildAddUserEvent(relaySecretKey []byte, userPubkey []byte) (*event.E, error) {
 154  	// Create signer
 155  	signer, err := p8k.New()
 156  	if err != nil {
 157  		return nil, err
 158  	}
 159  	if err = signer.InitSec(relaySecretKey); err != nil {
 160  		return nil, err
 161  	}
 162  
 163  	ev := event.New()
 164  	ev.Kind = KindAddUser
 165  	copy(ev.Pubkey, signer.Pub())
 166  
 167  	// Initialize tags
 168  	ev.Tags = tag.NewS()
 169  
 170  	// Add NIP-70 `-` tag
 171  	ev.Tags.Append(tag.NewFromAny("-"))
 172  
 173  	// Add p tag for the user
 174  	if len(userPubkey) == 32 {
 175  		ev.Tags.Append(tag.NewFromAny("p", hex.Enc(userPubkey)))
 176  	}
 177  
 178  	ev.CreatedAt = time.Now().Unix()
 179  	ev.Content = []byte("")
 180  
 181  	// Sign the event
 182  	if err := ev.Sign(signer); err != nil {
 183  		return nil, err
 184  	}
 185  
 186  	return ev, nil
 187  }
 188  
 189  // BuildRemoveUserEvent creates a kind 8001 remove user event
 190  func BuildRemoveUserEvent(relaySecretKey []byte, userPubkey []byte) (*event.E, error) {
 191  	// Create signer
 192  	signer, err := p8k.New()
 193  	if err != nil {
 194  		return nil, err
 195  	}
 196  	if err = signer.InitSec(relaySecretKey); err != nil {
 197  		return nil, err
 198  	}
 199  
 200  	ev := event.New()
 201  	ev.Kind = KindRemoveUser
 202  	copy(ev.Pubkey, signer.Pub())
 203  
 204  	// Initialize tags
 205  	ev.Tags = tag.NewS()
 206  
 207  	// Add NIP-70 `-` tag
 208  	ev.Tags.Append(tag.NewFromAny("-"))
 209  
 210  	// Add p tag for the user
 211  	if len(userPubkey) == 32 {
 212  		ev.Tags.Append(tag.NewFromAny("p", hex.Enc(userPubkey)))
 213  	}
 214  
 215  	ev.CreatedAt = time.Now().Unix()
 216  	ev.Content = []byte("")
 217  
 218  	// Sign the event
 219  	if err := ev.Sign(signer); err != nil {
 220  		return nil, err
 221  	}
 222  
 223  	return ev, nil
 224  }
 225  
 226  // BuildInviteEvent creates a kind 28935 invite event (ephemeral)
 227  func BuildInviteEvent(relaySecretKey []byte, inviteCode string) (*event.E, error) {
 228  	// Create signer
 229  	signer, err := p8k.New()
 230  	if err != nil {
 231  		return nil, err
 232  	}
 233  	if err = signer.InitSec(relaySecretKey); err != nil {
 234  		return nil, err
 235  	}
 236  
 237  	ev := event.New()
 238  	ev.Kind = KindInviteReq
 239  	copy(ev.Pubkey, signer.Pub())
 240  
 241  	// Initialize tags
 242  	ev.Tags = tag.NewS()
 243  
 244  	// Add NIP-70 `-` tag
 245  	ev.Tags.Append(tag.NewFromAny("-"))
 246  
 247  	// Add claim tag
 248  	ev.Tags.Append(tag.NewFromAny("claim", inviteCode))
 249  
 250  	ev.CreatedAt = time.Now().Unix()
 251  	ev.Content = []byte("")
 252  
 253  	// Sign the event
 254  	if err := ev.Sign(signer); err != nil {
 255  		return nil, err
 256  	}
 257  
 258  	return ev, nil
 259  }
 260  
 261  // ValidateJoinRequest validates a kind 28934 join request event
 262  func ValidateJoinRequest(ev *event.E) (inviteCode string, valid bool, reason string) {
 263  	// Must be kind 28934
 264  	if ev.Kind != KindJoinRequest {
 265  		return "", false, "invalid event kind"
 266  	}
 267  
 268  	// Must have NIP-70 `-` tag
 269  	hasMinusTag := ev.Tags.GetFirst([]byte("-")) != nil
 270  	if !hasMinusTag {
 271  		return "", false, "missing NIP-70 `-` tag"
 272  	}
 273  
 274  	// Must have claim tag
 275  	claimTag := ev.Tags.GetFirst([]byte("claim"))
 276  	if claimTag != nil && claimTag.Len() >= 2 {
 277  		inviteCode = string(claimTag.T[1])
 278  	}
 279  	if inviteCode == "" {
 280  		return "", false, "missing claim tag"
 281  	}
 282  
 283  	// Check timestamp (must be recent, within +/- 10 minutes)
 284  	now := time.Now().Unix()
 285  	if ev.CreatedAt < now-600 || ev.CreatedAt > now+600 {
 286  		return inviteCode, false, "timestamp out of range"
 287  	}
 288  
 289  	return inviteCode, true, ""
 290  }
 291  
 292  // ValidateLeaveRequest validates a kind 28936 leave request event
 293  func ValidateLeaveRequest(ev *event.E) (valid bool, reason string) {
 294  	// Must be kind 28936
 295  	if ev.Kind != KindLeaveRequest {
 296  		return false, "invalid event kind"
 297  	}
 298  
 299  	// Must have NIP-70 `-` tag
 300  	hasMinusTag := ev.Tags.GetFirst([]byte("-")) != nil
 301  	if !hasMinusTag {
 302  		return false, "missing NIP-70 `-` tag"
 303  	}
 304  
 305  	// Check timestamp (must be recent, within +/- 10 minutes)
 306  	now := time.Now().Unix()
 307  	if ev.CreatedAt < now-600 || ev.CreatedAt > now+600 {
 308  		return false, "timestamp out of range"
 309  	}
 310  
 311  	return true, ""
 312  }
 313