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.
268 lines
7.4 KiB
268 lines
7.4 KiB
package directory_client |
|
|
|
import ( |
|
"sync" |
|
|
|
"lol.mleku.dev/errorf" |
|
"next.orly.dev/pkg/encoders/event" |
|
"next.orly.dev/pkg/protocol/directory" |
|
) |
|
|
|
// IdentityResolver manages identity resolution and key delegation tracking. |
|
// |
|
// It maintains mappings between delegate keys and their primary identities, |
|
// enabling clients to resolve the actual identity behind any signing key. |
|
type IdentityResolver struct { |
|
mu sync.RWMutex |
|
|
|
// delegateToIdentity maps delegate public keys to their primary identity |
|
delegateToIdentity map[string]string |
|
|
|
// identityToDelegates maps primary identities to their delegate keys |
|
identityToDelegates map[string]map[string]bool |
|
|
|
// identityTagCache stores full identity tags by delegate key |
|
identityTagCache map[string]*directory.IdentityTag |
|
|
|
// publicKeyAds stores public key advertisements by key ID |
|
publicKeyAds map[string]*directory.PublicKeyAdvertisement |
|
} |
|
|
|
// NewIdentityResolver creates a new identity resolver instance. |
|
func NewIdentityResolver() *IdentityResolver { |
|
return &IdentityResolver{ |
|
delegateToIdentity: make(map[string]string), |
|
identityToDelegates: make(map[string]map[string]bool), |
|
identityTagCache: make(map[string]*directory.IdentityTag), |
|
publicKeyAds: make(map[string]*directory.PublicKeyAdvertisement), |
|
} |
|
} |
|
|
|
// ProcessEvent processes an event to extract and cache identity information. |
|
// |
|
// This should be called for all directory events to keep the resolver's |
|
// internal state up to date. |
|
func (r *IdentityResolver) ProcessEvent(ev *event.E) { |
|
if ev == nil { |
|
return |
|
} |
|
|
|
// Try to parse identity tag (I tag) |
|
identityTag := extractIdentityTag(ev) |
|
if identityTag != nil { |
|
r.cacheIdentityTag(identityTag) |
|
} |
|
|
|
// Handle public key advertisements specially |
|
if uint16(ev.Kind) == 39103 { |
|
if keyAd, err := directory.ParsePublicKeyAdvertisement(ev); err == nil { |
|
r.mu.Lock() |
|
r.publicKeyAds[keyAd.KeyID] = keyAd |
|
r.mu.Unlock() |
|
} |
|
} |
|
} |
|
|
|
// extractIdentityTag extracts an identity tag from an event if present. |
|
func extractIdentityTag(ev *event.E) *directory.IdentityTag { |
|
if ev == nil || ev.Tags == nil { |
|
return nil |
|
} |
|
|
|
// Find the I tag |
|
for _, t := range *ev.Tags { |
|
if t != nil && len(t.T) > 0 && string(t.T[0]) == "I" { |
|
if identityTag, err := directory.ParseIdentityTag(t); err == nil { |
|
return identityTag |
|
} |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
// cacheIdentityTag caches an identity tag mapping. |
|
func (r *IdentityResolver) cacheIdentityTag(tag *directory.IdentityTag) { |
|
if tag == nil { |
|
return |
|
} |
|
|
|
r.mu.Lock() |
|
defer r.mu.Unlock() |
|
|
|
identity := tag.NPubIdentity |
|
// For now, we use the identity as the delegate too since the structure is different |
|
// This should be updated when the IdentityTag structure is clarified |
|
delegate := identity |
|
|
|
// Store delegate -> identity mapping |
|
r.delegateToIdentity[delegate] = identity |
|
|
|
// Store identity -> delegates mapping |
|
if r.identityToDelegates[identity] == nil { |
|
r.identityToDelegates[identity] = make(map[string]bool) |
|
} |
|
r.identityToDelegates[identity][delegate] = true |
|
|
|
// Cache the full tag |
|
r.identityTagCache[delegate] = tag |
|
} |
|
|
|
// ResolveIdentity resolves the actual identity behind a public key. |
|
// |
|
// If the public key is a delegate, it returns the primary identity. |
|
// If the public key is already an identity, it returns the input unchanged. |
|
func (r *IdentityResolver) ResolveIdentity(pubkey string) string { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
if identity, ok := r.delegateToIdentity[pubkey]; ok { |
|
return identity |
|
} |
|
return pubkey |
|
} |
|
|
|
// ResolveEventIdentity resolves the actual identity behind an event's pubkey. |
|
func (r *IdentityResolver) ResolveEventIdentity(ev *event.E) string { |
|
if ev == nil { |
|
return "" |
|
} |
|
return r.ResolveIdentity(string(ev.Pubkey)) |
|
} |
|
|
|
// IsDelegateKey checks if a public key is a known delegate. |
|
func (r *IdentityResolver) IsDelegateKey(pubkey string) bool { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
_, ok := r.delegateToIdentity[pubkey] |
|
return ok |
|
} |
|
|
|
// IsIdentityKey checks if a public key is a known identity (has delegates). |
|
func (r *IdentityResolver) IsIdentityKey(pubkey string) bool { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
delegates, ok := r.identityToDelegates[pubkey] |
|
return ok && len(delegates) > 0 |
|
} |
|
|
|
// GetDelegatesForIdentity returns all delegate keys for a given identity. |
|
func (r *IdentityResolver) GetDelegatesForIdentity(identity string) (delegates []string) { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
delegateMap, ok := r.identityToDelegates[identity] |
|
if !ok { |
|
return []string{} |
|
} |
|
|
|
delegates = make([]string, 0, len(delegateMap)) |
|
for delegate := range delegateMap { |
|
delegates = append(delegates, delegate) |
|
} |
|
return |
|
} |
|
|
|
// GetIdentityTag returns the identity tag for a delegate key. |
|
func (r *IdentityResolver) GetIdentityTag(delegate string) (*directory.IdentityTag, error) { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
tag, ok := r.identityTagCache[delegate] |
|
if !ok { |
|
return nil, errorf.E("identity tag not found for delegate: %s", delegate) |
|
} |
|
return tag, nil |
|
} |
|
|
|
// GetPublicKeyAdvertisements returns all public key advertisements for an identity. |
|
func (r *IdentityResolver) GetPublicKeyAdvertisements(identity string) (ads []*directory.PublicKeyAdvertisement) { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
delegates := r.identityToDelegates[identity] |
|
ads = make([]*directory.PublicKeyAdvertisement, 0) |
|
|
|
for _, keyAd := range r.publicKeyAds { |
|
adIdentity := r.delegateToIdentity[string(keyAd.Event.Pubkey)] |
|
if adIdentity == "" { |
|
adIdentity = string(keyAd.Event.Pubkey) |
|
} |
|
|
|
if adIdentity == identity { |
|
ads = append(ads, keyAd) |
|
continue |
|
} |
|
|
|
// Check if the advertised key is a delegate |
|
if delegates != nil && delegates[keyAd.PublicKey] { |
|
ads = append(ads, keyAd) |
|
} |
|
} |
|
|
|
return |
|
} |
|
|
|
// GetPublicKeyAdvertisementByID returns a public key advertisement by key ID. |
|
func (r *IdentityResolver) GetPublicKeyAdvertisementByID(keyID string) (*directory.PublicKeyAdvertisement, error) { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
keyAd, ok := r.publicKeyAds[keyID] |
|
if !ok { |
|
return nil, errorf.E("public key advertisement not found: %s", keyID) |
|
} |
|
return keyAd, nil |
|
} |
|
|
|
// FilterEventsByIdentity filters events to only those signed by a specific identity or its delegates. |
|
func (r *IdentityResolver) FilterEventsByIdentity(events []*event.E, identity string) (filtered []*event.E) { |
|
r.mu.RLock() |
|
delegates := r.identityToDelegates[identity] |
|
r.mu.RUnlock() |
|
|
|
filtered = make([]*event.E, 0) |
|
for _, ev := range events { |
|
pubkey := string(ev.Pubkey) |
|
if pubkey == identity { |
|
filtered = append(filtered, ev) |
|
continue |
|
} |
|
if delegates != nil && delegates[pubkey] { |
|
filtered = append(filtered, ev) |
|
} |
|
} |
|
|
|
return |
|
} |
|
|
|
// ClearCache clears all cached identity mappings. |
|
func (r *IdentityResolver) ClearCache() { |
|
r.mu.Lock() |
|
defer r.mu.Unlock() |
|
|
|
r.delegateToIdentity = make(map[string]string) |
|
r.identityToDelegates = make(map[string]map[string]bool) |
|
r.identityTagCache = make(map[string]*directory.IdentityTag) |
|
r.publicKeyAds = make(map[string]*directory.PublicKeyAdvertisement) |
|
} |
|
|
|
// Stats returns statistics about tracked identities and delegates. |
|
type Stats struct { |
|
Identities int // Number of primary identities |
|
Delegates int // Number of delegate keys |
|
PublicKeyAds int // Number of public key advertisements |
|
} |
|
|
|
// GetStats returns statistics about the resolver's state. |
|
func (r *IdentityResolver) GetStats() Stats { |
|
r.mu.RLock() |
|
defer r.mu.RUnlock() |
|
|
|
return Stats{ |
|
Identities: len(r.identityToDelegates), |
|
Delegates: len(r.delegateToIdentity), |
|
PublicKeyAds: len(r.publicKeyAds), |
|
} |
|
}
|
|
|