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