implemented and tested NIP-43 invite based ACL
This commit is contained in:
259
pkg/database/nip43.go
Normal file
259
pkg/database/nip43.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
)
|
||||
|
||||
// NIP43Membership represents membership metadata for NIP-43
|
||||
type NIP43Membership struct {
|
||||
Pubkey []byte
|
||||
AddedAt time.Time
|
||||
InviteCode string
|
||||
}
|
||||
|
||||
// Database key prefixes for NIP-43
|
||||
const (
|
||||
nip43MemberPrefix = "nip43:member:"
|
||||
nip43InvitePrefix = "nip43:invite:"
|
||||
)
|
||||
|
||||
// AddNIP43Member adds a member to the NIP-43 membership list
|
||||
func (d *D) AddNIP43Member(pubkey []byte, inviteCode string) error {
|
||||
if len(pubkey) != 32 {
|
||||
return fmt.Errorf("invalid pubkey length: %d", len(pubkey))
|
||||
}
|
||||
|
||||
key := append([]byte(nip43MemberPrefix), pubkey...)
|
||||
|
||||
// Create membership record
|
||||
membership := NIP43Membership{
|
||||
Pubkey: pubkey,
|
||||
AddedAt: time.Now(),
|
||||
InviteCode: inviteCode,
|
||||
}
|
||||
|
||||
// Serialize membership data
|
||||
val := serializeNIP43Membership(membership)
|
||||
|
||||
return d.DB.Update(func(txn *badger.Txn) error {
|
||||
return txn.Set(key, val)
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveNIP43Member removes a member from the NIP-43 membership list
|
||||
func (d *D) RemoveNIP43Member(pubkey []byte) error {
|
||||
if len(pubkey) != 32 {
|
||||
return fmt.Errorf("invalid pubkey length: %d", len(pubkey))
|
||||
}
|
||||
|
||||
key := append([]byte(nip43MemberPrefix), pubkey...)
|
||||
|
||||
return d.DB.Update(func(txn *badger.Txn) error {
|
||||
return txn.Delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
// IsNIP43Member checks if a pubkey is a NIP-43 member
|
||||
func (d *D) IsNIP43Member(pubkey []byte) (isMember bool, err error) {
|
||||
if len(pubkey) != 32 {
|
||||
return false, fmt.Errorf("invalid pubkey length: %d", len(pubkey))
|
||||
}
|
||||
|
||||
key := append([]byte(nip43MemberPrefix), pubkey...)
|
||||
|
||||
err = d.DB.View(func(txn *badger.Txn) error {
|
||||
_, err := txn.Get(key)
|
||||
if err == badger.ErrKeyNotFound {
|
||||
isMember = false
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isMember = true
|
||||
return nil
|
||||
})
|
||||
|
||||
return isMember, err
|
||||
}
|
||||
|
||||
// GetNIP43Membership retrieves membership details for a pubkey
|
||||
func (d *D) GetNIP43Membership(pubkey []byte) (*NIP43Membership, error) {
|
||||
if len(pubkey) != 32 {
|
||||
return nil, fmt.Errorf("invalid pubkey length: %d", len(pubkey))
|
||||
}
|
||||
|
||||
key := append([]byte(nip43MemberPrefix), pubkey...)
|
||||
var membership *NIP43Membership
|
||||
|
||||
err := d.DB.View(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return item.Value(func(val []byte) error {
|
||||
membership = deserializeNIP43Membership(val)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return membership, nil
|
||||
}
|
||||
|
||||
// GetAllNIP43Members returns all NIP-43 members
|
||||
func (d *D) GetAllNIP43Members() ([][]byte, error) {
|
||||
var members [][]byte
|
||||
prefix := []byte(nip43MemberPrefix)
|
||||
|
||||
err := d.DB.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.Prefix = prefix
|
||||
opts.PrefetchValues = false // We only need keys
|
||||
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
|
||||
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
|
||||
item := it.Item()
|
||||
key := item.Key()
|
||||
// Extract pubkey from key (skip prefix)
|
||||
pubkey := make([]byte, 32)
|
||||
copy(pubkey, key[len(prefix):])
|
||||
members = append(members, pubkey)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return members, err
|
||||
}
|
||||
|
||||
// StoreInviteCode stores an invite code with expiry
|
||||
func (d *D) StoreInviteCode(code string, expiresAt time.Time) error {
|
||||
key := append([]byte(nip43InvitePrefix), []byte(code)...)
|
||||
|
||||
// Serialize expiry time as unix timestamp
|
||||
val := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(val, uint64(expiresAt.Unix()))
|
||||
|
||||
return d.DB.Update(func(txn *badger.Txn) error {
|
||||
entry := badger.NewEntry(key, val).WithTTL(time.Until(expiresAt))
|
||||
return txn.SetEntry(entry)
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateInviteCode checks if an invite code is valid and not expired
|
||||
func (d *D) ValidateInviteCode(code string) (valid bool, err error) {
|
||||
key := append([]byte(nip43InvitePrefix), []byte(code)...)
|
||||
|
||||
err = d.DB.View(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get(key)
|
||||
if err == badger.ErrKeyNotFound {
|
||||
valid = false
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return item.Value(func(val []byte) error {
|
||||
if len(val) != 8 {
|
||||
return fmt.Errorf("invalid invite code value")
|
||||
}
|
||||
expiresAt := int64(binary.BigEndian.Uint64(val))
|
||||
valid = time.Now().Unix() < expiresAt
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
return valid, err
|
||||
}
|
||||
|
||||
// DeleteInviteCode removes an invite code (after use)
|
||||
func (d *D) DeleteInviteCode(code string) error {
|
||||
key := append([]byte(nip43InvitePrefix), []byte(code)...)
|
||||
|
||||
return d.DB.Update(func(txn *badger.Txn) error {
|
||||
return txn.Delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions for serialization
|
||||
|
||||
func serializeNIP43Membership(m NIP43Membership) []byte {
|
||||
// Format: [pubkey(32)] [timestamp(8)] [invite_code_len(2)] [invite_code]
|
||||
codeBytes := []byte(m.InviteCode)
|
||||
codeLen := len(codeBytes)
|
||||
|
||||
buf := make([]byte, 32+8+2+codeLen)
|
||||
|
||||
// Copy pubkey
|
||||
copy(buf[0:32], m.Pubkey)
|
||||
|
||||
// Write timestamp
|
||||
binary.BigEndian.PutUint64(buf[32:40], uint64(m.AddedAt.Unix()))
|
||||
|
||||
// Write invite code length
|
||||
binary.BigEndian.PutUint16(buf[40:42], uint16(codeLen))
|
||||
|
||||
// Write invite code
|
||||
copy(buf[42:], codeBytes)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func deserializeNIP43Membership(data []byte) *NIP43Membership {
|
||||
if len(data) < 42 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := &NIP43Membership{}
|
||||
|
||||
// Read pubkey
|
||||
m.Pubkey = make([]byte, 32)
|
||||
copy(m.Pubkey, data[0:32])
|
||||
|
||||
// Read timestamp
|
||||
timestamp := binary.BigEndian.Uint64(data[32:40])
|
||||
m.AddedAt = time.Unix(int64(timestamp), 0)
|
||||
|
||||
// Read invite code
|
||||
codeLen := binary.BigEndian.Uint16(data[40:42])
|
||||
if len(data) >= 42+int(codeLen) {
|
||||
m.InviteCode = string(data[42 : 42+codeLen])
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// PublishNIP43MembershipEvent publishes membership change events
|
||||
func (d *D) PublishNIP43MembershipEvent(kind int, pubkey []byte) error {
|
||||
log.I.F("publishing NIP-43 event kind %d for pubkey %s", kind, hex.Enc(pubkey))
|
||||
|
||||
// Get relay identity
|
||||
relaySecret, err := d.GetOrCreateRelayIdentitySecret()
|
||||
if chk.E(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// This would integrate with the event publisher
|
||||
// For now, just log it
|
||||
log.D.F("would publish kind %d event for member %s", kind, hex.Enc(pubkey))
|
||||
|
||||
// The actual publishing will be done by the handler
|
||||
_ = relaySecret
|
||||
|
||||
return nil
|
||||
}
|
||||
406
pkg/database/nip43_test.go
Normal file
406
pkg/database/nip43_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setupNIP43TestDB(t *testing.T) (*D, func()) {
|
||||
tempDir, err := os.MkdirTemp("", "nip43_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
db, err := New(ctx, cancel, tempDir, "info")
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
t.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
db.Close()
|
||||
os.RemoveAll(tempDir)
|
||||
}
|
||||
|
||||
return db, cleanup
|
||||
}
|
||||
|
||||
// TestAddNIP43Member tests adding a member
|
||||
func TestAddNIP43Member(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
pubkey := make([]byte, 32)
|
||||
for i := range pubkey {
|
||||
pubkey[i] = byte(i)
|
||||
}
|
||||
inviteCode := "test-invite-123"
|
||||
|
||||
err := db.AddNIP43Member(pubkey, inviteCode)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add member: %v", err)
|
||||
}
|
||||
|
||||
// Verify member was added
|
||||
isMember, err := db.IsNIP43Member(pubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check membership: %v", err)
|
||||
}
|
||||
if !isMember {
|
||||
t.Error("member was not added")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddNIP43Member_InvalidPubkey tests adding member with invalid pubkey
|
||||
func TestAddNIP43Member_InvalidPubkey(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Test with wrong length
|
||||
invalidPubkey := make([]byte, 16)
|
||||
err := db.AddNIP43Member(invalidPubkey, "test-code")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid pubkey length")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoveNIP43Member tests removing a member
|
||||
func TestRemoveNIP43Member(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
pubkey := make([]byte, 32)
|
||||
for i := range pubkey {
|
||||
pubkey[i] = byte(i)
|
||||
}
|
||||
|
||||
// Add member
|
||||
err := db.AddNIP43Member(pubkey, "test-code")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add member: %v", err)
|
||||
}
|
||||
|
||||
// Remove member
|
||||
err = db.RemoveNIP43Member(pubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to remove member: %v", err)
|
||||
}
|
||||
|
||||
// Verify member was removed
|
||||
isMember, err := db.IsNIP43Member(pubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check membership: %v", err)
|
||||
}
|
||||
if isMember {
|
||||
t.Error("member was not removed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsNIP43Member tests membership checking
|
||||
func TestIsNIP43Member(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
pubkey := make([]byte, 32)
|
||||
for i := range pubkey {
|
||||
pubkey[i] = byte(i)
|
||||
}
|
||||
|
||||
// Check non-existent member
|
||||
isMember, err := db.IsNIP43Member(pubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check membership: %v", err)
|
||||
}
|
||||
if isMember {
|
||||
t.Error("non-existent member reported as member")
|
||||
}
|
||||
|
||||
// Add member
|
||||
err = db.AddNIP43Member(pubkey, "test-code")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add member: %v", err)
|
||||
}
|
||||
|
||||
// Check existing member
|
||||
isMember, err = db.IsNIP43Member(pubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check membership: %v", err)
|
||||
}
|
||||
if !isMember {
|
||||
t.Error("existing member not found")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetNIP43Membership tests retrieving membership details
|
||||
func TestGetNIP43Membership(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
pubkey := make([]byte, 32)
|
||||
for i := range pubkey {
|
||||
pubkey[i] = byte(i)
|
||||
}
|
||||
inviteCode := "test-invite-abc123"
|
||||
|
||||
// Add member
|
||||
beforeAdd := time.Now()
|
||||
err := db.AddNIP43Member(pubkey, inviteCode)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add member: %v", err)
|
||||
}
|
||||
afterAdd := time.Now()
|
||||
|
||||
// Get membership
|
||||
membership, err := db.GetNIP43Membership(pubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get membership: %v", err)
|
||||
}
|
||||
|
||||
// Verify details
|
||||
if len(membership.Pubkey) != 32 {
|
||||
t.Errorf("wrong pubkey length: got %d, want 32", len(membership.Pubkey))
|
||||
}
|
||||
for i := range pubkey {
|
||||
if membership.Pubkey[i] != pubkey[i] {
|
||||
t.Errorf("pubkey mismatch at index %d", i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if membership.InviteCode != inviteCode {
|
||||
t.Errorf("invite code mismatch: got %s, want %s", membership.InviteCode, inviteCode)
|
||||
}
|
||||
|
||||
// Allow some tolerance for timestamp (database operations may take time)
|
||||
if membership.AddedAt.Before(beforeAdd.Add(-5*time.Second)) || membership.AddedAt.After(afterAdd.Add(5*time.Second)) {
|
||||
t.Errorf("AddedAt timestamp out of expected range: got %v, expected between %v and %v",
|
||||
membership.AddedAt, beforeAdd, afterAdd)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAllNIP43Members tests retrieving all members
|
||||
func TestGetAllNIP43Members(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Add multiple members
|
||||
memberCount := 5
|
||||
for i := 0; i < memberCount; i++ {
|
||||
pubkey := make([]byte, 32)
|
||||
for j := range pubkey {
|
||||
pubkey[j] = byte(i*10 + j)
|
||||
}
|
||||
err := db.AddNIP43Member(pubkey, "code-"+string(rune(i)))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add member %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all members
|
||||
members, err := db.GetAllNIP43Members()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get all members: %v", err)
|
||||
}
|
||||
|
||||
if len(members) != memberCount {
|
||||
t.Errorf("wrong member count: got %d, want %d", len(members), memberCount)
|
||||
}
|
||||
|
||||
// Verify each member has valid pubkey
|
||||
for i, member := range members {
|
||||
if len(member) != 32 {
|
||||
t.Errorf("member %d has invalid pubkey length: %d", i, len(member))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoreInviteCode tests storing invite codes
|
||||
func TestStoreInviteCode(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
code := "test-invite-xyz789"
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
err := db.StoreInviteCode(code, expiresAt)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to store invite code: %v", err)
|
||||
}
|
||||
|
||||
// Validate the code
|
||||
valid, err := db.ValidateInviteCode(code)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to validate invite code: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Error("stored invite code is not valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateInviteCode_Expired tests expired invite code handling
|
||||
func TestValidateInviteCode_Expired(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
code := "expired-code"
|
||||
expiresAt := time.Now().Add(-1 * time.Hour) // Already expired
|
||||
|
||||
err := db.StoreInviteCode(code, expiresAt)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to store invite code: %v", err)
|
||||
}
|
||||
|
||||
// Validate the code - should be invalid because it's expired
|
||||
valid, err := db.ValidateInviteCode(code)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to validate invite code: %v", err)
|
||||
}
|
||||
if valid {
|
||||
t.Error("expired invite code reported as valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateInviteCode_NonExistent tests non-existent code validation
|
||||
func TestValidateInviteCode_NonExistent(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
valid, err := db.ValidateInviteCode("non-existent-code")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if valid {
|
||||
t.Error("non-existent code reported as valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteInviteCode tests deleting invite codes
|
||||
func TestDeleteInviteCode(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
code := "delete-me-code"
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
// Store code
|
||||
err := db.StoreInviteCode(code, expiresAt)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to store invite code: %v", err)
|
||||
}
|
||||
|
||||
// Verify it exists
|
||||
valid, err := db.ValidateInviteCode(code)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to validate invite code: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Error("stored code is not valid")
|
||||
}
|
||||
|
||||
// Delete code
|
||||
err = db.DeleteInviteCode(code)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to delete invite code: %v", err)
|
||||
}
|
||||
|
||||
// Verify it's gone
|
||||
valid, err = db.ValidateInviteCode(code)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to validate after delete: %v", err)
|
||||
}
|
||||
if valid {
|
||||
t.Error("deleted code still valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNIP43Membership_Serialization tests membership serialization
|
||||
func TestNIP43Membership_Serialization(t *testing.T) {
|
||||
pubkey := make([]byte, 32)
|
||||
for i := range pubkey {
|
||||
pubkey[i] = byte(i)
|
||||
}
|
||||
|
||||
original := NIP43Membership{
|
||||
Pubkey: pubkey,
|
||||
AddedAt: time.Now(),
|
||||
InviteCode: "test-code-123",
|
||||
}
|
||||
|
||||
// Serialize
|
||||
data := serializeNIP43Membership(original)
|
||||
|
||||
// Deserialize
|
||||
deserialized := deserializeNIP43Membership(data)
|
||||
|
||||
// Verify
|
||||
if deserialized == nil {
|
||||
t.Fatal("deserialization returned nil")
|
||||
}
|
||||
|
||||
if len(deserialized.Pubkey) != 32 {
|
||||
t.Errorf("wrong pubkey length: got %d, want 32", len(deserialized.Pubkey))
|
||||
}
|
||||
|
||||
for i := range pubkey {
|
||||
if deserialized.Pubkey[i] != pubkey[i] {
|
||||
t.Errorf("pubkey mismatch at index %d", i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if deserialized.InviteCode != original.InviteCode {
|
||||
t.Errorf("invite code mismatch: got %s, want %s", deserialized.InviteCode, original.InviteCode)
|
||||
}
|
||||
|
||||
// Allow 1 second tolerance for timestamp comparison (due to Unix conversion)
|
||||
timeDiff := deserialized.AddedAt.Sub(original.AddedAt)
|
||||
if timeDiff < -1*time.Second || timeDiff > 1*time.Second {
|
||||
t.Errorf("timestamp mismatch: got %v, want %v (diff: %v)", deserialized.AddedAt, original.AddedAt, timeDiff)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNIP43Membership_ConcurrentAccess tests concurrent access to membership
|
||||
func TestNIP43Membership_ConcurrentAccess(t *testing.T) {
|
||||
db, cleanup := setupNIP43TestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
const goroutines = 10
|
||||
const membersPerGoroutine = 5
|
||||
|
||||
done := make(chan bool, goroutines)
|
||||
|
||||
// Add members concurrently
|
||||
for g := 0; g < goroutines; g++ {
|
||||
go func(offset int) {
|
||||
for i := 0; i < membersPerGoroutine; i++ {
|
||||
pubkey := make([]byte, 32)
|
||||
for j := range pubkey {
|
||||
pubkey[j] = byte((offset*membersPerGoroutine+i)*10 + j)
|
||||
}
|
||||
if err := db.AddNIP43Member(pubkey, "code"); err != nil {
|
||||
t.Errorf("failed to add member: %v", err)
|
||||
}
|
||||
}
|
||||
done <- true
|
||||
}(g)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < goroutines; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify all members were added
|
||||
members, err := db.GetAllNIP43Members()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get all members: %v", err)
|
||||
}
|
||||
|
||||
expected := goroutines * membersPerGoroutine
|
||||
if len(members) != expected {
|
||||
t.Errorf("wrong member count: got %d, want %d", len(members), expected)
|
||||
}
|
||||
}
|
||||
312
pkg/protocol/nip43/types.go
Normal file
312
pkg/protocol/nip43/types.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package nip43
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/encoders/tag"
|
||||
"next.orly.dev/pkg/interfaces/signer/p8k"
|
||||
)
|
||||
|
||||
// Event kinds defined by NIP-43
|
||||
const (
|
||||
KindMemberList = 13534 // Membership list published by relay
|
||||
KindAddUser = 8000 // Add user event published by relay
|
||||
KindRemoveUser = 8001 // Remove user event published by relay
|
||||
KindJoinRequest = 28934 // Join request sent by user
|
||||
KindInviteReq = 28935 // Invite request (ephemeral)
|
||||
KindLeaveRequest = 28936 // Leave request sent by user
|
||||
)
|
||||
|
||||
// InviteCode represents a claim/invite code for relay access
|
||||
type InviteCode struct {
|
||||
Code string
|
||||
ExpiresAt time.Time
|
||||
UsedBy []byte // pubkey that used this code, nil if unused
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// InviteManager manages invite codes for NIP-43
|
||||
type InviteManager struct {
|
||||
mu sync.RWMutex
|
||||
codes map[string]*InviteCode
|
||||
expiry time.Duration
|
||||
}
|
||||
|
||||
// NewInviteManager creates a new invite code manager
|
||||
func NewInviteManager(expiryDuration time.Duration) *InviteManager {
|
||||
if expiryDuration == 0 {
|
||||
expiryDuration = 24 * time.Hour // Default: 24 hours
|
||||
}
|
||||
return &InviteManager{
|
||||
codes: make(map[string]*InviteCode),
|
||||
expiry: expiryDuration,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateCode creates a new invite code
|
||||
func (im *InviteManager) GenerateCode() (code string, err error) {
|
||||
// Generate 32 random bytes
|
||||
b := make([]byte, 32)
|
||||
if _, err = rand.Read(b); err != nil {
|
||||
return
|
||||
}
|
||||
code = base64.URLEncoding.EncodeToString(b)
|
||||
|
||||
im.mu.Lock()
|
||||
defer im.mu.Unlock()
|
||||
|
||||
im.codes[code] = &InviteCode{
|
||||
Code: code,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(im.expiry),
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// ValidateAndConsume validates an invite code and marks it as used by the given pubkey
|
||||
func (im *InviteManager) ValidateAndConsume(code string, pubkey []byte) (valid bool, reason string) {
|
||||
im.mu.Lock()
|
||||
defer im.mu.Unlock()
|
||||
|
||||
invite, exists := im.codes[code]
|
||||
if !exists {
|
||||
return false, "invalid invite code"
|
||||
}
|
||||
|
||||
if time.Now().After(invite.ExpiresAt) {
|
||||
delete(im.codes, code)
|
||||
return false, "invite code expired"
|
||||
}
|
||||
|
||||
if invite.UsedBy != nil {
|
||||
return false, "invite code already used"
|
||||
}
|
||||
|
||||
// Mark as used
|
||||
invite.UsedBy = make([]byte, len(pubkey))
|
||||
copy(invite.UsedBy, pubkey)
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// CleanupExpired removes expired invite codes
|
||||
func (im *InviteManager) CleanupExpired() {
|
||||
im.mu.Lock()
|
||||
defer im.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for code, invite := range im.codes {
|
||||
if now.After(invite.ExpiresAt) {
|
||||
delete(im.codes, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuildMemberListEvent creates a kind 13534 membership list event
|
||||
// relaySecretKey: the relay's identity secret key (32 bytes)
|
||||
// members: list of member pubkeys (32 bytes each)
|
||||
func BuildMemberListEvent(relaySecretKey []byte, members [][]byte) (*event.E, error) {
|
||||
// Create signer
|
||||
signer, err := p8k.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = signer.InitSec(relaySecretKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = KindMemberList
|
||||
copy(ev.Pubkey, signer.Pub())
|
||||
|
||||
// Initialize tags
|
||||
ev.Tags = tag.NewS()
|
||||
|
||||
// Add NIP-70 `-` tag
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
|
||||
// Add member tags
|
||||
for _, member := range members {
|
||||
if len(member) == 32 {
|
||||
ev.Tags.Append(tag.NewFromAny("member", hex.Enc(member)))
|
||||
}
|
||||
}
|
||||
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Content = []byte("")
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// BuildAddUserEvent creates a kind 8000 add user event
|
||||
func BuildAddUserEvent(relaySecretKey []byte, userPubkey []byte) (*event.E, error) {
|
||||
// Create signer
|
||||
signer, err := p8k.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = signer.InitSec(relaySecretKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = KindAddUser
|
||||
copy(ev.Pubkey, signer.Pub())
|
||||
|
||||
// Initialize tags
|
||||
ev.Tags = tag.NewS()
|
||||
|
||||
// Add NIP-70 `-` tag
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
|
||||
// Add p tag for the user
|
||||
if len(userPubkey) == 32 {
|
||||
ev.Tags.Append(tag.NewFromAny("p", hex.Enc(userPubkey)))
|
||||
}
|
||||
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Content = []byte("")
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// BuildRemoveUserEvent creates a kind 8001 remove user event
|
||||
func BuildRemoveUserEvent(relaySecretKey []byte, userPubkey []byte) (*event.E, error) {
|
||||
// Create signer
|
||||
signer, err := p8k.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = signer.InitSec(relaySecretKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = KindRemoveUser
|
||||
copy(ev.Pubkey, signer.Pub())
|
||||
|
||||
// Initialize tags
|
||||
ev.Tags = tag.NewS()
|
||||
|
||||
// Add NIP-70 `-` tag
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
|
||||
// Add p tag for the user
|
||||
if len(userPubkey) == 32 {
|
||||
ev.Tags.Append(tag.NewFromAny("p", hex.Enc(userPubkey)))
|
||||
}
|
||||
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Content = []byte("")
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// BuildInviteEvent creates a kind 28935 invite event (ephemeral)
|
||||
func BuildInviteEvent(relaySecretKey []byte, inviteCode string) (*event.E, error) {
|
||||
// Create signer
|
||||
signer, err := p8k.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = signer.InitSec(relaySecretKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = KindInviteReq
|
||||
copy(ev.Pubkey, signer.Pub())
|
||||
|
||||
// Initialize tags
|
||||
ev.Tags = tag.NewS()
|
||||
|
||||
// Add NIP-70 `-` tag
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
|
||||
// Add claim tag
|
||||
ev.Tags.Append(tag.NewFromAny("claim", inviteCode))
|
||||
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Content = []byte("")
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// ValidateJoinRequest validates a kind 28934 join request event
|
||||
func ValidateJoinRequest(ev *event.E) (inviteCode string, valid bool, reason string) {
|
||||
// Must be kind 28934
|
||||
if ev.Kind != KindJoinRequest {
|
||||
return "", false, "invalid event kind"
|
||||
}
|
||||
|
||||
// Must have NIP-70 `-` tag
|
||||
hasMinusTag := ev.Tags.GetFirst([]byte("-")) != nil
|
||||
if !hasMinusTag {
|
||||
return "", false, "missing NIP-70 `-` tag"
|
||||
}
|
||||
|
||||
// Must have claim tag
|
||||
claimTag := ev.Tags.GetFirst([]byte("claim"))
|
||||
if claimTag != nil && claimTag.Len() >= 2 {
|
||||
inviteCode = string(claimTag.T[1])
|
||||
}
|
||||
if inviteCode == "" {
|
||||
return "", false, "missing claim tag"
|
||||
}
|
||||
|
||||
// Check timestamp (must be recent, within +/- 10 minutes)
|
||||
now := time.Now().Unix()
|
||||
if ev.CreatedAt < now-600 || ev.CreatedAt > now+600 {
|
||||
return inviteCode, false, "timestamp out of range"
|
||||
}
|
||||
|
||||
return inviteCode, true, ""
|
||||
}
|
||||
|
||||
// ValidateLeaveRequest validates a kind 28936 leave request event
|
||||
func ValidateLeaveRequest(ev *event.E) (valid bool, reason string) {
|
||||
// Must be kind 28936
|
||||
if ev.Kind != KindLeaveRequest {
|
||||
return false, "invalid event kind"
|
||||
}
|
||||
|
||||
// Must have NIP-70 `-` tag
|
||||
hasMinusTag := ev.Tags.GetFirst([]byte("-")) != nil
|
||||
if !hasMinusTag {
|
||||
return false, "missing NIP-70 `-` tag"
|
||||
}
|
||||
|
||||
// Check timestamp (must be recent, within +/- 10 minutes)
|
||||
now := time.Now().Unix()
|
||||
if ev.CreatedAt < now-600 || ev.CreatedAt > now+600 {
|
||||
return false, "timestamp out of range"
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
514
pkg/protocol/nip43/types_test.go
Normal file
514
pkg/protocol/nip43/types_test.go
Normal file
@@ -0,0 +1,514 @@
|
||||
package nip43
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/crypto/keys"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/tag"
|
||||
)
|
||||
|
||||
// TestInviteManager_GenerateCode tests invite code generation
|
||||
func TestInviteManager_GenerateCode(t *testing.T) {
|
||||
im := NewInviteManager(24 * time.Hour)
|
||||
|
||||
code, err := im.GenerateCode()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate code: %v", err)
|
||||
}
|
||||
|
||||
if code == "" {
|
||||
t.Fatal("generated code is empty")
|
||||
}
|
||||
|
||||
// Verify the code exists in the manager
|
||||
im.mu.Lock()
|
||||
invite, exists := im.codes[code]
|
||||
im.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatal("generated code not found in manager")
|
||||
}
|
||||
|
||||
if invite.Code != code {
|
||||
t.Errorf("code mismatch: got %s, want %s", invite.Code, code)
|
||||
}
|
||||
|
||||
if invite.UsedBy != nil {
|
||||
t.Error("newly generated code should not be used")
|
||||
}
|
||||
|
||||
if time.Until(invite.ExpiresAt) > 24*time.Hour {
|
||||
t.Error("expiry time is too far in the future")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInviteManager_ValidateAndConsume tests invite code validation
|
||||
func TestInviteManager_ValidateAndConsume(t *testing.T) {
|
||||
im := NewInviteManager(24 * time.Hour)
|
||||
|
||||
// Generate a code
|
||||
code, err := im.GenerateCode()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate code: %v", err)
|
||||
}
|
||||
|
||||
testPubkey := make([]byte, 32)
|
||||
for i := range testPubkey {
|
||||
testPubkey[i] = byte(i)
|
||||
}
|
||||
|
||||
// Test valid code
|
||||
valid, reason := im.ValidateAndConsume(code, testPubkey)
|
||||
if !valid {
|
||||
t.Fatalf("valid code rejected: %s", reason)
|
||||
}
|
||||
|
||||
// Test already used code
|
||||
valid, reason = im.ValidateAndConsume(code, testPubkey)
|
||||
if valid {
|
||||
t.Error("already used code was accepted")
|
||||
}
|
||||
if reason != "invite code already used" {
|
||||
t.Errorf("wrong rejection reason: got %s", reason)
|
||||
}
|
||||
|
||||
// Test invalid code
|
||||
valid, reason = im.ValidateAndConsume("invalid-code", testPubkey)
|
||||
if valid {
|
||||
t.Error("invalid code was accepted")
|
||||
}
|
||||
if reason != "invalid invite code" {
|
||||
t.Errorf("wrong rejection reason: got %s", reason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInviteManager_ExpiredCode tests expired invite code handling
|
||||
func TestInviteManager_ExpiredCode(t *testing.T) {
|
||||
// Create manager with very short expiry
|
||||
im := NewInviteManager(1 * time.Millisecond)
|
||||
|
||||
code, err := im.GenerateCode()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate code: %v", err)
|
||||
}
|
||||
|
||||
// Wait for expiry
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
testPubkey := make([]byte, 32)
|
||||
valid, reason := im.ValidateAndConsume(code, testPubkey)
|
||||
if valid {
|
||||
t.Error("expired code was accepted")
|
||||
}
|
||||
if reason != "invite code expired" {
|
||||
t.Errorf("wrong rejection reason: got %s, want 'invite code expired'", reason)
|
||||
}
|
||||
|
||||
// Verify code was deleted
|
||||
im.mu.Lock()
|
||||
_, exists := im.codes[code]
|
||||
im.mu.Unlock()
|
||||
|
||||
if exists {
|
||||
t.Error("expired code was not deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInviteManager_CleanupExpired tests cleanup of expired codes
|
||||
func TestInviteManager_CleanupExpired(t *testing.T) {
|
||||
im := NewInviteManager(1 * time.Millisecond)
|
||||
|
||||
// Generate multiple codes
|
||||
codes := make([]string, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
code, err := im.GenerateCode()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate code %d: %v", i, err)
|
||||
}
|
||||
codes[i] = code
|
||||
}
|
||||
|
||||
// Wait for expiry
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Cleanup
|
||||
im.CleanupExpired()
|
||||
|
||||
// Verify all codes were deleted
|
||||
im.mu.Lock()
|
||||
remaining := len(im.codes)
|
||||
im.mu.Unlock()
|
||||
|
||||
if remaining != 0 {
|
||||
t.Errorf("cleanup failed: %d codes remaining", remaining)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildMemberListEvent tests membership list event creation
|
||||
func TestBuildMemberListEvent(t *testing.T) {
|
||||
// Generate a test relay secret
|
||||
relaySecret, err := keys.GenerateSecretKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate relay secret: %v", err)
|
||||
}
|
||||
|
||||
// Create test member pubkeys
|
||||
members := make([][]byte, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
members[i] = make([]byte, 32)
|
||||
for j := range members[i] {
|
||||
members[i][j] = byte(i*10 + j)
|
||||
}
|
||||
}
|
||||
|
||||
// Build event
|
||||
ev, err := BuildMemberListEvent(relaySecret, members)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build member list event: %v", err)
|
||||
}
|
||||
|
||||
// Verify event kind
|
||||
if ev.Kind != KindMemberList {
|
||||
t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindMemberList)
|
||||
}
|
||||
|
||||
// Verify NIP-70 tag
|
||||
minusTag := ev.Tags.GetFirst([]byte("-"))
|
||||
if minusTag == nil {
|
||||
t.Error("missing NIP-70 `-` tag")
|
||||
}
|
||||
|
||||
// Verify member tags
|
||||
memberTags := ev.Tags.GetAll([]byte("member"))
|
||||
if len(memberTags) != 3 {
|
||||
t.Errorf("wrong number of member tags: got %d, want 3", len(memberTags))
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid, err := ev.Verify()
|
||||
if err != nil {
|
||||
t.Fatalf("signature verification error: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Error("event signature is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildAddUserEvent tests add user event creation
|
||||
func TestBuildAddUserEvent(t *testing.T) {
|
||||
relaySecret, err := keys.GenerateSecretKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate relay secret: %v", err)
|
||||
}
|
||||
|
||||
userPubkey := make([]byte, 32)
|
||||
for i := range userPubkey {
|
||||
userPubkey[i] = byte(i)
|
||||
}
|
||||
|
||||
ev, err := BuildAddUserEvent(relaySecret, userPubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build add user event: %v", err)
|
||||
}
|
||||
|
||||
// Verify event kind
|
||||
if ev.Kind != KindAddUser {
|
||||
t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindAddUser)
|
||||
}
|
||||
|
||||
// Verify NIP-70 tag
|
||||
minusTag := ev.Tags.GetFirst([]byte("-"))
|
||||
if minusTag == nil {
|
||||
t.Error("missing NIP-70 `-` tag")
|
||||
}
|
||||
|
||||
// Verify p tag
|
||||
pTag := ev.Tags.GetFirst([]byte("p"))
|
||||
if pTag == nil {
|
||||
t.Error("missing p tag")
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid, err := ev.Verify()
|
||||
if err != nil {
|
||||
t.Fatalf("signature verification error: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Error("event signature is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildRemoveUserEvent tests remove user event creation
|
||||
func TestBuildRemoveUserEvent(t *testing.T) {
|
||||
relaySecret, err := keys.GenerateSecretKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate relay secret: %v", err)
|
||||
}
|
||||
|
||||
userPubkey := make([]byte, 32)
|
||||
for i := range userPubkey {
|
||||
userPubkey[i] = byte(i)
|
||||
}
|
||||
|
||||
ev, err := BuildRemoveUserEvent(relaySecret, userPubkey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build remove user event: %v", err)
|
||||
}
|
||||
|
||||
// Verify event kind
|
||||
if ev.Kind != KindRemoveUser {
|
||||
t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindRemoveUser)
|
||||
}
|
||||
|
||||
// Verify NIP-70 tag
|
||||
minusTag := ev.Tags.GetFirst([]byte("-"))
|
||||
if minusTag == nil {
|
||||
t.Error("missing NIP-70 `-` tag")
|
||||
}
|
||||
|
||||
// Verify p tag
|
||||
pTag := ev.Tags.GetFirst([]byte("p"))
|
||||
if pTag == nil {
|
||||
t.Error("missing p tag")
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid, err := ev.Verify()
|
||||
if err != nil {
|
||||
t.Fatalf("signature verification error: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Error("event signature is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildInviteEvent tests invite event creation
|
||||
func TestBuildInviteEvent(t *testing.T) {
|
||||
relaySecret, err := keys.GenerateSecretKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate relay secret: %v", err)
|
||||
}
|
||||
|
||||
inviteCode := "test-invite-code-12345"
|
||||
|
||||
ev, err := BuildInviteEvent(relaySecret, inviteCode)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build invite event: %v", err)
|
||||
}
|
||||
|
||||
// Verify event kind
|
||||
if ev.Kind != KindInviteReq {
|
||||
t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindInviteReq)
|
||||
}
|
||||
|
||||
// Verify NIP-70 tag
|
||||
minusTag := ev.Tags.GetFirst([]byte("-"))
|
||||
if minusTag == nil {
|
||||
t.Error("missing NIP-70 `-` tag")
|
||||
}
|
||||
|
||||
// Verify claim tag
|
||||
claimTag := ev.Tags.GetFirst([]byte("claim"))
|
||||
if claimTag == nil {
|
||||
t.Error("missing claim tag")
|
||||
}
|
||||
if claimTag.Len() < 2 {
|
||||
t.Error("claim tag has no value")
|
||||
}
|
||||
if string(claimTag.T[1]) != inviteCode {
|
||||
t.Errorf("wrong invite code in tag: got %s, want %s", string(claimTag.T[1]), inviteCode)
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid, err := ev.Verify()
|
||||
if err != nil {
|
||||
t.Fatalf("signature verification error: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Error("event signature is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateJoinRequest tests join request validation
|
||||
func TestValidateJoinRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupEvent func() *event.E
|
||||
expectValid bool
|
||||
expectCode string
|
||||
expectReason string
|
||||
}{
|
||||
{
|
||||
name: "valid join request",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindJoinRequest
|
||||
ev.Tags = tag.NewS()
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
ev.Tags.Append(tag.NewFromAny("claim", "test-code-123"))
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
return ev
|
||||
},
|
||||
expectValid: true,
|
||||
expectCode: "test-code-123",
|
||||
expectReason: "",
|
||||
},
|
||||
{
|
||||
name: "wrong kind",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = 1000
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectReason: "invalid event kind",
|
||||
},
|
||||
{
|
||||
name: "missing minus tag",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindJoinRequest
|
||||
ev.Tags = tag.NewS()
|
||||
ev.Tags.Append(tag.NewFromAny("claim", "test-code"))
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectReason: "missing NIP-70 `-` tag",
|
||||
},
|
||||
{
|
||||
name: "missing claim tag",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindJoinRequest
|
||||
ev.Tags = tag.NewS()
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectReason: "missing claim tag",
|
||||
},
|
||||
{
|
||||
name: "timestamp too old",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindJoinRequest
|
||||
ev.Tags = tag.NewS()
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
ev.Tags.Append(tag.NewFromAny("claim", "test-code"))
|
||||
ev.CreatedAt = time.Now().Unix() - 700 // More than 10 minutes ago
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectCode: "test-code",
|
||||
expectReason: "timestamp out of range",
|
||||
},
|
||||
{
|
||||
name: "timestamp too far in future",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindJoinRequest
|
||||
ev.Tags = tag.NewS()
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
ev.Tags.Append(tag.NewFromAny("claim", "test-code"))
|
||||
ev.CreatedAt = time.Now().Unix() + 700 // More than 10 minutes ahead
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectCode: "test-code",
|
||||
expectReason: "timestamp out of range",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ev := tt.setupEvent()
|
||||
code, valid, reason := ValidateJoinRequest(ev)
|
||||
|
||||
if valid != tt.expectValid {
|
||||
t.Errorf("valid mismatch: got %v, want %v", valid, tt.expectValid)
|
||||
}
|
||||
if tt.expectCode != "" && code != tt.expectCode {
|
||||
t.Errorf("code mismatch: got %s, want %s", code, tt.expectCode)
|
||||
}
|
||||
if tt.expectReason != "" && reason != tt.expectReason {
|
||||
t.Errorf("reason mismatch: got %s, want %s", reason, tt.expectReason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateLeaveRequest tests leave request validation
|
||||
func TestValidateLeaveRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupEvent func() *event.E
|
||||
expectValid bool
|
||||
expectReason string
|
||||
}{
|
||||
{
|
||||
name: "valid leave request",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindLeaveRequest
|
||||
ev.Tags = tag.NewS()
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
return ev
|
||||
},
|
||||
expectValid: true,
|
||||
expectReason: "",
|
||||
},
|
||||
{
|
||||
name: "wrong kind",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = 1000
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectReason: "invalid event kind",
|
||||
},
|
||||
{
|
||||
name: "missing minus tag",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindLeaveRequest
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectReason: "missing NIP-70 `-` tag",
|
||||
},
|
||||
{
|
||||
name: "timestamp out of range",
|
||||
setupEvent: func() *event.E {
|
||||
ev := event.New()
|
||||
ev.Kind = KindLeaveRequest
|
||||
ev.Tags = tag.NewS()
|
||||
ev.Tags.Append(tag.NewFromAny("-"))
|
||||
ev.CreatedAt = time.Now().Unix() - 700
|
||||
return ev
|
||||
},
|
||||
expectValid: false,
|
||||
expectReason: "timestamp out of range",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ev := tt.setupEvent()
|
||||
valid, reason := ValidateLeaveRequest(ev)
|
||||
|
||||
if valid != tt.expectValid {
|
||||
t.Errorf("valid mismatch: got %v, want %v", valid, tt.expectValid)
|
||||
}
|
||||
if tt.expectReason != "" && reason != tt.expectReason {
|
||||
t.Errorf("reason mismatch: got %s, want %s", reason, tt.expectReason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,8 @@ var (
|
||||
NIP40 = ExpirationTimestamp
|
||||
Authentication = NIP{"Authentication of clients to relays", 42}
|
||||
NIP42 = Authentication
|
||||
RelayAccessMetadata = NIP{"Relay Access Metadata and Requests", 43}
|
||||
NIP43 = RelayAccessMetadata
|
||||
VersionedEncryption = NIP{"Encrypted Payloads (Versioned)", 44}
|
||||
NIP44 = VersionedEncryption
|
||||
CountingResults = NIP{"Counting results", 45}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
v0.26.4
|
||||
Reference in New Issue
Block a user