identity_resolver.go raw

   1  package directory_client
   2  
   3  import (
   4  	"sync"
   5  
   6  	"next.orly.dev/pkg/lol/errorf"
   7  	"next.orly.dev/pkg/nostr/encoders/event"
   8  	"next.orly.dev/pkg/protocol/directory"
   9  )
  10  
  11  // IdentityResolver manages identity resolution and key delegation tracking.
  12  //
  13  // It maintains mappings between delegate keys and their primary identities,
  14  // enabling clients to resolve the actual identity behind any signing key.
  15  type IdentityResolver struct {
  16  	mu sync.RWMutex
  17  
  18  	// delegateToIdentity maps delegate public keys to their primary identity
  19  	delegateToIdentity map[string]string
  20  
  21  	// identityToDelegates maps primary identities to their delegate keys
  22  	identityToDelegates map[string]map[string]bool
  23  
  24  	// identityTagCache stores full identity tags by delegate key
  25  	identityTagCache map[string]*directory.IdentityTag
  26  
  27  	// publicKeyAds stores public key advertisements by key ID
  28  	publicKeyAds map[string]*directory.PublicKeyAdvertisement
  29  }
  30  
  31  // NewIdentityResolver creates a new identity resolver instance.
  32  func NewIdentityResolver() *IdentityResolver {
  33  	return &IdentityResolver{
  34  		delegateToIdentity:  make(map[string]string),
  35  		identityToDelegates: make(map[string]map[string]bool),
  36  		identityTagCache:    make(map[string]*directory.IdentityTag),
  37  		publicKeyAds:        make(map[string]*directory.PublicKeyAdvertisement),
  38  	}
  39  }
  40  
  41  // ProcessEvent processes an event to extract and cache identity information.
  42  //
  43  // This should be called for all directory events to keep the resolver's
  44  // internal state up to date.
  45  func (r *IdentityResolver) ProcessEvent(ev *event.E) {
  46  	if ev == nil {
  47  		return
  48  	}
  49  
  50  	// Try to parse identity tag (I tag)
  51  	identityTag := extractIdentityTag(ev)
  52  	if identityTag != nil {
  53  		r.cacheIdentityTag(identityTag)
  54  	}
  55  
  56  	// Handle public key advertisements specially
  57  	if uint16(ev.Kind) == 39103 {
  58  		if keyAd, err := directory.ParsePublicKeyAdvertisement(ev); err == nil {
  59  			r.mu.Lock()
  60  			r.publicKeyAds[keyAd.KeyID] = keyAd
  61  			r.mu.Unlock()
  62  		}
  63  	}
  64  }
  65  
  66  // extractIdentityTag extracts an identity tag from an event if present.
  67  func extractIdentityTag(ev *event.E) *directory.IdentityTag {
  68  	if ev == nil || ev.Tags == nil {
  69  		return nil
  70  	}
  71  
  72  	// Find the I tag
  73  	for _, t := range *ev.Tags {
  74  		if t != nil && len(t.T) > 0 && string(t.T[0]) == "I" {
  75  			if identityTag, err := directory.ParseIdentityTag(t); err == nil {
  76  				return identityTag
  77  			}
  78  		}
  79  	}
  80  	return nil
  81  }
  82  
  83  // cacheIdentityTag caches an identity tag mapping.
  84  func (r *IdentityResolver) cacheIdentityTag(tag *directory.IdentityTag) {
  85  	if tag == nil {
  86  		return
  87  	}
  88  
  89  	r.mu.Lock()
  90  	defer r.mu.Unlock()
  91  
  92  	identity := tag.NPubIdentity
  93  	// For now, we use the identity as the delegate too since the structure is different
  94  	// This should be updated when the IdentityTag structure is clarified
  95  	delegate := identity
  96  
  97  	// Store delegate -> identity mapping
  98  	r.delegateToIdentity[delegate] = identity
  99  
 100  	// Store identity -> delegates mapping
 101  	if r.identityToDelegates[identity] == nil {
 102  		r.identityToDelegates[identity] = make(map[string]bool)
 103  	}
 104  	r.identityToDelegates[identity][delegate] = true
 105  
 106  	// Cache the full tag
 107  	r.identityTagCache[delegate] = tag
 108  }
 109  
 110  // ResolveIdentity resolves the actual identity behind a public key.
 111  //
 112  // If the public key is a delegate, it returns the primary identity.
 113  // If the public key is already an identity, it returns the input unchanged.
 114  func (r *IdentityResolver) ResolveIdentity(pubkey string) string {
 115  	r.mu.RLock()
 116  	defer r.mu.RUnlock()
 117  
 118  	if identity, ok := r.delegateToIdentity[pubkey]; ok {
 119  		return identity
 120  	}
 121  	return pubkey
 122  }
 123  
 124  // ResolveEventIdentity resolves the actual identity behind an event's pubkey.
 125  func (r *IdentityResolver) ResolveEventIdentity(ev *event.E) string {
 126  	if ev == nil {
 127  		return ""
 128  	}
 129  	return r.ResolveIdentity(string(ev.Pubkey))
 130  }
 131  
 132  // IsDelegateKey checks if a public key is a known delegate.
 133  func (r *IdentityResolver) IsDelegateKey(pubkey string) bool {
 134  	r.mu.RLock()
 135  	defer r.mu.RUnlock()
 136  
 137  	_, ok := r.delegateToIdentity[pubkey]
 138  	return ok
 139  }
 140  
 141  // IsIdentityKey checks if a public key is a known identity (has delegates).
 142  func (r *IdentityResolver) IsIdentityKey(pubkey string) bool {
 143  	r.mu.RLock()
 144  	defer r.mu.RUnlock()
 145  
 146  	delegates, ok := r.identityToDelegates[pubkey]
 147  	return ok && len(delegates) > 0
 148  }
 149  
 150  // GetDelegatesForIdentity returns all delegate keys for a given identity.
 151  func (r *IdentityResolver) GetDelegatesForIdentity(identity string) (delegates []string) {
 152  	r.mu.RLock()
 153  	defer r.mu.RUnlock()
 154  
 155  	delegateMap, ok := r.identityToDelegates[identity]
 156  	if !ok {
 157  		return []string{}
 158  	}
 159  
 160  	delegates = make([]string, 0, len(delegateMap))
 161  	for delegate := range delegateMap {
 162  		delegates = append(delegates, delegate)
 163  	}
 164  	return
 165  }
 166  
 167  // GetIdentityTag returns the identity tag for a delegate key.
 168  func (r *IdentityResolver) GetIdentityTag(delegate string) (*directory.IdentityTag, error) {
 169  	r.mu.RLock()
 170  	defer r.mu.RUnlock()
 171  
 172  	tag, ok := r.identityTagCache[delegate]
 173  	if !ok {
 174  		return nil, errorf.E("identity tag not found for delegate: %s", delegate)
 175  	}
 176  	return tag, nil
 177  }
 178  
 179  // GetPublicKeyAdvertisements returns all public key advertisements for an identity.
 180  func (r *IdentityResolver) GetPublicKeyAdvertisements(identity string) (ads []*directory.PublicKeyAdvertisement) {
 181  	r.mu.RLock()
 182  	defer r.mu.RUnlock()
 183  
 184  	delegates := r.identityToDelegates[identity]
 185  	ads = make([]*directory.PublicKeyAdvertisement, 0)
 186  
 187  	for _, keyAd := range r.publicKeyAds {
 188  		adIdentity := r.delegateToIdentity[string(keyAd.Event.Pubkey)]
 189  		if adIdentity == "" {
 190  			adIdentity = string(keyAd.Event.Pubkey)
 191  		}
 192  
 193  		if adIdentity == identity {
 194  			ads = append(ads, keyAd)
 195  			continue
 196  		}
 197  
 198  		// Check if the advertised key is a delegate
 199  		if delegates != nil && delegates[keyAd.PublicKey] {
 200  			ads = append(ads, keyAd)
 201  		}
 202  	}
 203  
 204  	return
 205  }
 206  
 207  // GetPublicKeyAdvertisementByID returns a public key advertisement by key ID.
 208  func (r *IdentityResolver) GetPublicKeyAdvertisementByID(keyID string) (*directory.PublicKeyAdvertisement, error) {
 209  	r.mu.RLock()
 210  	defer r.mu.RUnlock()
 211  
 212  	keyAd, ok := r.publicKeyAds[keyID]
 213  	if !ok {
 214  		return nil, errorf.E("public key advertisement not found: %s", keyID)
 215  	}
 216  	return keyAd, nil
 217  }
 218  
 219  // FilterEventsByIdentity filters events to only those signed by a specific identity or its delegates.
 220  func (r *IdentityResolver) FilterEventsByIdentity(events []*event.E, identity string) (filtered []*event.E) {
 221  	r.mu.RLock()
 222  	delegates := r.identityToDelegates[identity]
 223  	r.mu.RUnlock()
 224  
 225  	filtered = make([]*event.E, 0)
 226  	for _, ev := range events {
 227  		pubkey := string(ev.Pubkey)
 228  		if pubkey == identity {
 229  			filtered = append(filtered, ev)
 230  			continue
 231  		}
 232  		if delegates != nil && delegates[pubkey] {
 233  			filtered = append(filtered, ev)
 234  		}
 235  	}
 236  
 237  	return
 238  }
 239  
 240  // ClearCache clears all cached identity mappings.
 241  func (r *IdentityResolver) ClearCache() {
 242  	r.mu.Lock()
 243  	defer r.mu.Unlock()
 244  
 245  	r.delegateToIdentity = make(map[string]string)
 246  	r.identityToDelegates = make(map[string]map[string]bool)
 247  	r.identityTagCache = make(map[string]*directory.IdentityTag)
 248  	r.publicKeyAds = make(map[string]*directory.PublicKeyAdvertisement)
 249  }
 250  
 251  // Stats returns statistics about tracked identities and delegates.
 252  type Stats struct {
 253  	Identities   int // Number of primary identities
 254  	Delegates    int // Number of delegate keys
 255  	PublicKeyAds int // Number of public key advertisements
 256  }
 257  
 258  // GetStats returns statistics about the resolver's state.
 259  func (r *IdentityResolver) GetStats() Stats {
 260  	r.mu.RLock()
 261  	defer r.mu.RUnlock()
 262  
 263  	return Stats{
 264  		Identities:   len(r.identityToDelegates),
 265  		Delegates:    len(r.delegateToIdentity),
 266  		PublicKeyAds: len(r.publicKeyAds),
 267  	}
 268  }
 269