nip43.go raw

   1  //go:build js && wasm
   2  
   3  package wasmdb
   4  
   5  import (
   6  	"bytes"
   7  	"encoding/binary"
   8  	"errors"
   9  	"time"
  10  
  11  	"github.com/aperturerobotics/go-indexeddb/idb"
  12  	"github.com/hack-pad/safejs"
  13  
  14  	"next.orly.dev/pkg/database"
  15  )
  16  
  17  const (
  18  	// NIP43StoreName is the object store for NIP-43 membership
  19  	NIP43StoreName = "nip43"
  20  
  21  	// InvitesStoreName is the object store for invite codes
  22  	InvitesStoreName = "invites"
  23  )
  24  
  25  // AddNIP43Member adds a pubkey as a NIP-43 member with the given invite code
  26  func (w *W) AddNIP43Member(pubkey []byte, inviteCode string) error {
  27  	if len(pubkey) != 32 {
  28  		return errors.New("invalid pubkey length")
  29  	}
  30  
  31  	// Create membership record
  32  	membership := &database.NIP43Membership{
  33  		Pubkey:     make([]byte, 32),
  34  		InviteCode: inviteCode,
  35  		AddedAt:    time.Now(),
  36  	}
  37  	copy(membership.Pubkey, pubkey)
  38  
  39  	// Serialize membership
  40  	data := w.serializeNIP43Membership(membership)
  41  
  42  	// Store using pubkey as key
  43  	return w.setStoreValue(NIP43StoreName, string(pubkey), data)
  44  }
  45  
  46  // RemoveNIP43Member removes a pubkey from NIP-43 membership
  47  func (w *W) RemoveNIP43Member(pubkey []byte) error {
  48  	return w.deleteStoreValue(NIP43StoreName, string(pubkey))
  49  }
  50  
  51  // IsNIP43Member checks if a pubkey is a NIP-43 member
  52  func (w *W) IsNIP43Member(pubkey []byte) (isMember bool, err error) {
  53  	data, err := w.getStoreValue(NIP43StoreName, string(pubkey))
  54  	if err != nil {
  55  		return false, nil // Not found is not an error, just not a member
  56  	}
  57  	return data != nil, nil
  58  }
  59  
  60  // GetNIP43Membership returns the full membership details for a pubkey
  61  func (w *W) GetNIP43Membership(pubkey []byte) (*database.NIP43Membership, error) {
  62  	data, err := w.getStoreValue(NIP43StoreName, string(pubkey))
  63  	if err != nil {
  64  		return nil, err
  65  	}
  66  	if data == nil {
  67  		return nil, errors.New("membership not found")
  68  	}
  69  
  70  	return w.deserializeNIP43Membership(data)
  71  }
  72  
  73  // GetAllNIP43Members returns all NIP-43 member pubkeys
  74  func (w *W) GetAllNIP43Members() ([][]byte, error) {
  75  	tx, err := w.db.Transaction(idb.TransactionReadOnly, NIP43StoreName)
  76  	if err != nil {
  77  		return nil, err
  78  	}
  79  
  80  	store, err := tx.ObjectStore(NIP43StoreName)
  81  	if err != nil {
  82  		return nil, err
  83  	}
  84  
  85  	var members [][]byte
  86  
  87  	cursorReq, err := store.OpenCursor(idb.CursorNext)
  88  	if err != nil {
  89  		return nil, err
  90  	}
  91  
  92  	err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
  93  		keyVal, keyErr := cursor.Key()
  94  		if keyErr != nil {
  95  			return keyErr
  96  		}
  97  
  98  		// Key is the pubkey stored as string
  99  		keyBytes := safeValueToBytes(keyVal)
 100  		if len(keyBytes) == 32 {
 101  			pubkey := make([]byte, 32)
 102  			copy(pubkey, keyBytes)
 103  			members = append(members, pubkey)
 104  		}
 105  
 106  		return cursor.Continue()
 107  	})
 108  
 109  	if err != nil && err.Error() != "found" {
 110  		return nil, err
 111  	}
 112  
 113  	return members, nil
 114  }
 115  
 116  // StoreInviteCode stores an invite code with expiration time
 117  func (w *W) StoreInviteCode(code string, expiresAt time.Time) error {
 118  	// Serialize expiration time as unix timestamp
 119  	data := make([]byte, 8)
 120  	binary.BigEndian.PutUint64(data, uint64(expiresAt.Unix()))
 121  
 122  	return w.setStoreValue(InvitesStoreName, code, data)
 123  }
 124  
 125  // ValidateInviteCode checks if an invite code is valid (exists and not expired)
 126  func (w *W) ValidateInviteCode(code string) (valid bool, err error) {
 127  	data, err := w.getStoreValue(InvitesStoreName, code)
 128  	if err != nil {
 129  		return false, nil
 130  	}
 131  	if data == nil || len(data) < 8 {
 132  		return false, nil
 133  	}
 134  
 135  	// Check expiration
 136  	expiresAt := time.Unix(int64(binary.BigEndian.Uint64(data)), 0)
 137  	if time.Now().After(expiresAt) {
 138  		return false, nil
 139  	}
 140  
 141  	return true, nil
 142  }
 143  
 144  // DeleteInviteCode removes an invite code
 145  func (w *W) DeleteInviteCode(code string) error {
 146  	return w.deleteStoreValue(InvitesStoreName, code)
 147  }
 148  
 149  // PublishNIP43MembershipEvent is a no-op in WASM (events are handled by the relay)
 150  func (w *W) PublishNIP43MembershipEvent(kind int, pubkey []byte) error {
 151  	// In WASM context, this would typically be handled by the client
 152  	// This is a no-op implementation
 153  	return nil
 154  }
 155  
 156  // serializeNIP43Membership converts a membership to bytes for storage
 157  func (w *W) serializeNIP43Membership(m *database.NIP43Membership) []byte {
 158  	buf := new(bytes.Buffer)
 159  
 160  	// Write pubkey (32 bytes)
 161  	buf.Write(m.Pubkey)
 162  
 163  	// Write AddedAt as unix timestamp (8 bytes)
 164  	ts := make([]byte, 8)
 165  	binary.BigEndian.PutUint64(ts, uint64(m.AddedAt.Unix()))
 166  	buf.Write(ts)
 167  
 168  	// Write invite code length (4 bytes) + invite code
 169  	codeBytes := []byte(m.InviteCode)
 170  	codeLen := make([]byte, 4)
 171  	binary.BigEndian.PutUint32(codeLen, uint32(len(codeBytes)))
 172  	buf.Write(codeLen)
 173  	buf.Write(codeBytes)
 174  
 175  	return buf.Bytes()
 176  }
 177  
 178  // deserializeNIP43Membership converts bytes back to a membership
 179  func (w *W) deserializeNIP43Membership(data []byte) (*database.NIP43Membership, error) {
 180  	if len(data) < 44 { // 32 + 8 + 4 minimum
 181  		return nil, errors.New("invalid membership data")
 182  	}
 183  
 184  	m := &database.NIP43Membership{}
 185  
 186  	// Read pubkey
 187  	m.Pubkey = make([]byte, 32)
 188  	copy(m.Pubkey, data[:32])
 189  
 190  	// Read AddedAt
 191  	m.AddedAt = time.Unix(int64(binary.BigEndian.Uint64(data[32:40])), 0)
 192  
 193  	// Read invite code
 194  	codeLen := binary.BigEndian.Uint32(data[40:44])
 195  	if len(data) < int(44+codeLen) {
 196  		return nil, errors.New("invalid invite code length")
 197  	}
 198  	m.InviteCode = string(data[44 : 44+codeLen])
 199  
 200  	return m, nil
 201  }
 202  
 203  // Helper to convert safejs.Value to string for keys
 204  func safeValueToString(v safejs.Value) string {
 205  	if v.IsUndefined() || v.IsNull() {
 206  		return ""
 207  	}
 208  	str, err := v.String()
 209  	if err != nil {
 210  		return ""
 211  	}
 212  	return str
 213  }
 214