identity-resolver.ts raw
1 /**
2 * Identity Resolution for Directory Consensus Protocol
3 *
4 * This module provides functionality to resolve actual identities behind
5 * delegate keys and manage key delegations.
6 */
7
8 import type { EventStore } from 'applesauce-core';
9 import type { NostrEvent } from 'applesauce-core/helpers';
10 import type { IdentityTag, PublicKeyAdvertisement } from './types.js';
11 import { EventKinds } from './types.js';
12 import { parseIdentityTag, parsePublicKeyAdvertisement } from './parsers.js';
13 import { ValidationError } from './validation.js';
14 import { Observable, combineLatest, map, startWith } from 'rxjs';
15
16 /**
17 * Manages identity resolution and key delegation tracking
18 */
19 export class IdentityResolver {
20 private eventStore: EventStore;
21 private delegateToIdentity: Map<string, string> = new Map();
22 private identityToDelegates: Map<string, Set<string>> = new Map();
23 private identityTagCache: Map<string, IdentityTag> = new Map();
24 private publicKeyAds: Map<string, PublicKeyAdvertisement> = new Map();
25
26 constructor(eventStore: EventStore) {
27 this.eventStore = eventStore;
28 this.initializeTracking();
29 }
30
31 /**
32 * Initialize tracking of identity tags and key delegations
33 */
34 private initializeTracking(): void {
35 // Track all events with I tags
36 this.eventStore.stream({ kinds: Object.values(EventKinds) }).subscribe(event => {
37 this.processEvent(event);
38 });
39
40 // Track Public Key Advertisements (kind 39103)
41 this.eventStore.stream({ kinds: [EventKinds.PublicKeyAdvertisement] }).subscribe(event => {
42 try {
43 const keyAd = parsePublicKeyAdvertisement(event);
44 this.publicKeyAds.set(keyAd.keyID, keyAd);
45 } catch (err) {
46 // Ignore invalid events
47 console.warn('Invalid public key advertisement:', err);
48 }
49 });
50 }
51
52 /**
53 * Process an event to extract and cache identity information
54 */
55 private processEvent(event: NostrEvent): void {
56 try {
57 const identityTag = parseIdentityTag(event);
58 if (identityTag) {
59 this.cacheIdentityTag(identityTag);
60 }
61 } catch (err) {
62 // Event doesn't have a valid I tag or parsing failed
63 }
64 }
65
66 /**
67 * Cache an identity tag mapping
68 */
69 private cacheIdentityTag(tag: IdentityTag): void {
70 const { identity, delegate } = tag;
71
72 // Store delegate -> identity mapping
73 this.delegateToIdentity.set(delegate, identity);
74
75 // Store identity -> delegates mapping
76 if (!this.identityToDelegates.has(identity)) {
77 this.identityToDelegates.set(identity, new Set());
78 }
79 this.identityToDelegates.get(identity)!.add(delegate);
80
81 // Cache the full tag
82 this.identityTagCache.set(delegate, tag);
83 }
84
85 /**
86 * Resolve the actual identity behind a public key (which may be a delegate)
87 *
88 * @param pubkey - The public key to resolve (may be delegate or identity)
89 * @returns The actual identity public key, or the input if it's already an identity
90 */
91 public resolveIdentity(pubkey: string): string {
92 return this.delegateToIdentity.get(pubkey) || pubkey;
93 }
94
95 /**
96 * Resolve the actual identity behind an event's pubkey
97 *
98 * @param event - The event to resolve
99 * @returns The actual identity public key
100 */
101 public resolveEventIdentity(event: NostrEvent): string {
102 return this.resolveIdentity(event.pubkey);
103 }
104
105 /**
106 * Check if a public key is a known delegate
107 *
108 * @param pubkey - The public key to check
109 * @returns true if the key is a delegate, false otherwise
110 */
111 public isDelegateKey(pubkey: string): boolean {
112 return this.delegateToIdentity.has(pubkey);
113 }
114
115 /**
116 * Check if a public key is a known identity (has delegates)
117 *
118 * @param pubkey - The public key to check
119 * @returns true if the key is an identity with delegates, false otherwise
120 */
121 public isIdentityKey(pubkey: string): boolean {
122 return this.identityToDelegates.has(pubkey);
123 }
124
125 /**
126 * Get all delegate keys for a given identity
127 *
128 * @param identity - The identity public key
129 * @returns Set of delegate public keys
130 */
131 public getDelegatesForIdentity(identity: string): Set<string> {
132 return this.identityToDelegates.get(identity) || new Set();
133 }
134
135 /**
136 * Get the identity tag for a delegate key
137 *
138 * @param delegate - The delegate public key
139 * @returns The identity tag, or undefined if not found
140 */
141 public getIdentityTag(delegate: string): IdentityTag | undefined {
142 return this.identityTagCache.get(delegate);
143 }
144
145 /**
146 * Get all public key advertisements for an identity
147 *
148 * @param identity - The identity public key
149 * @returns Array of public key advertisements
150 */
151 public getPublicKeyAdvertisements(identity: string): PublicKeyAdvertisement[] {
152 const delegates = this.getDelegatesForIdentity(identity);
153 const ads: PublicKeyAdvertisement[] = [];
154
155 for (const keyAd of this.publicKeyAds.values()) {
156 const adIdentity = this.resolveIdentity(keyAd.event.pubkey);
157 if (adIdentity === identity || delegates.has(keyAd.publicKey)) {
158 ads.push(keyAd);
159 }
160 }
161
162 return ads;
163 }
164
165 /**
166 * Get a public key advertisement by key ID
167 *
168 * @param keyID - The unique key identifier
169 * @returns The public key advertisement, or undefined if not found
170 */
171 public getPublicKeyAdvertisementByID(keyID: string): PublicKeyAdvertisement | undefined {
172 return this.publicKeyAds.get(keyID);
173 }
174
175 /**
176 * Stream all events by their actual identity
177 *
178 * @param identity - The identity public key
179 * @param includeNewEvents - If true, include future events (default: false)
180 * @returns Observable of events signed by this identity or its delegates
181 */
182 public streamEventsByIdentity(identity: string, includeNewEvents = false): Observable<NostrEvent> {
183 const delegates = this.getDelegatesForIdentity(identity);
184 const allKeys = [identity, ...Array.from(delegates)];
185
186 return this.eventStore.stream(
187 { authors: allKeys },
188 includeNewEvents
189 );
190 }
191
192 /**
193 * Stream events by identity with real-time delegate updates
194 *
195 * This will automatically include events from newly discovered delegates.
196 *
197 * @param identity - The identity public key
198 * @returns Observable of events signed by this identity or its delegates
199 */
200 public streamEventsByIdentityLive(identity: string): Observable<NostrEvent> {
201 // Create an observable that emits whenever delegates change
202 const delegateUpdates$ = new Observable<Set<string>>(observer => {
203 // Emit initial delegates
204 observer.next(this.getDelegatesForIdentity(identity));
205
206 // Watch for new delegates
207 const subscription = this.eventStore.stream({ kinds: Object.values(EventKinds) }, true)
208 .subscribe(event => {
209 try {
210 const identityTag = parseIdentityTag(event);
211 if (identityTag && identityTag.identity === identity) {
212 this.cacheIdentityTag(identityTag);
213 observer.next(this.getDelegatesForIdentity(identity));
214 }
215 } catch (err) {
216 // Ignore invalid events
217 }
218 });
219
220 return () => subscription.unsubscribe();
221 });
222
223 // Map delegate updates to event streams
224 return delegateUpdates$.pipe(
225 map(delegates => {
226 const allKeys = [identity, ...Array.from(delegates)];
227 return this.eventStore.stream({ authors: allKeys }, true);
228 }),
229 // Flatten the nested observable
230 map(stream$ => stream$),
231 ) as any; // Type assertion needed due to complex Observable nesting
232 }
233
234 /**
235 * Verify that an identity tag signature is valid
236 *
237 * Note: This requires schnorr signature verification which should be
238 * implemented using appropriate cryptographic libraries.
239 *
240 * @param tag - The identity tag to verify
241 * @returns Promise that resolves to true if valid, false otherwise
242 */
243 public async verifyIdentityTag(tag: IdentityTag): Promise<boolean> {
244 // TODO: Implement schnorr signature verification
245 // The signature is over: sha256(identity + delegate + relayHint)
246 //
247 // Example implementation would require:
248 // 1. Concatenate: identity + delegate + (relayHint || '')
249 // 2. Compute SHA256 hash
250 // 3. Verify signature using identity key
251
252 throw new Error('Identity tag verification not yet implemented');
253 }
254
255 /**
256 * Clear all cached identity mappings
257 */
258 public clearCache(): void {
259 this.delegateToIdentity.clear();
260 this.identityToDelegates.clear();
261 this.identityTagCache.clear();
262 this.publicKeyAds.clear();
263 }
264
265 /**
266 * Get statistics about tracked identities and delegates
267 */
268 public getStats(): {
269 identities: number;
270 delegates: number;
271 publicKeyAds: number;
272 } {
273 return {
274 identities: this.identityToDelegates.size,
275 delegates: this.delegateToIdentity.size,
276 publicKeyAds: this.publicKeyAds.size,
277 };
278 }
279 }
280
281 /**
282 * Helper function to create an identity resolver instance
283 */
284 export function createIdentityResolver(eventStore: EventStore): IdentityResolver {
285 return new IdentityResolver(eventStore);
286 }
287
288