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