logger.service.ts raw

   1  /* eslint-disable @typescript-eslint/no-explicit-any */
   2  import { Injectable } from '@angular/core';
   3  
   4  declare const chrome: any;
   5  
   6  export type LogCategory =
   7    | 'nip07'
   8    | 'permission'
   9    | 'vault'
  10    | 'profile'
  11    | 'bookmark'
  12    | 'system';
  13  
  14  export interface LogEntry {
  15    timestamp: Date;
  16    level: 'log' | 'warn' | 'error' | 'debug';
  17    category: LogCategory;
  18    icon: string;
  19    message: string;
  20    data?: any;
  21  }
  22  
  23  // Serializable format for storage
  24  interface StoredLogEntry {
  25    timestamp: string;
  26    level: 'log' | 'warn' | 'error' | 'debug';
  27    category: LogCategory;
  28    icon: string;
  29    message: string;
  30    data?: any;
  31  }
  32  
  33  const LOGS_STORAGE_KEY = 'extensionLogs';
  34  
  35  @Injectable({
  36    providedIn: 'root',
  37  })
  38  export class LoggerService {
  39    #namespace: string | undefined;
  40    #logs: LogEntry[] = [];
  41    #maxLogs = 500;
  42  
  43    get logs(): LogEntry[] {
  44      return this.#logs;
  45    }
  46  
  47    async initialize(namespace: string): Promise<void> {
  48      this.#namespace = namespace;
  49      await this.#loadLogsFromStorage();
  50    }
  51  
  52    async #loadLogsFromStorage(): Promise<void> {
  53      try {
  54        if (typeof chrome !== 'undefined' && chrome.storage?.session) {
  55          const result = await chrome.storage.session.get(LOGS_STORAGE_KEY);
  56          if (result[LOGS_STORAGE_KEY]) {
  57            // Convert stored format back to LogEntry with Date objects
  58            this.#logs = (result[LOGS_STORAGE_KEY] as StoredLogEntry[]).map(
  59              (entry) => ({
  60                ...entry,
  61                timestamp: new Date(entry.timestamp),
  62              })
  63            );
  64          }
  65        }
  66      } catch (error) {
  67        console.error('Failed to load logs from storage:', error);
  68      }
  69    }
  70  
  71    async #saveLogsToStorage(): Promise<void> {
  72      try {
  73        if (typeof chrome !== 'undefined' && chrome.storage?.session) {
  74          // Convert Date to ISO string for storage
  75          const storedLogs: StoredLogEntry[] = this.#logs.map((entry) => ({
  76            ...entry,
  77            timestamp: entry.timestamp.toISOString(),
  78          }));
  79          await chrome.storage.session.set({ [LOGS_STORAGE_KEY]: storedLogs });
  80        }
  81      } catch (error) {
  82        console.error('Failed to save logs to storage:', error);
  83      }
  84    }
  85  
  86    async refreshLogs(): Promise<void> {
  87      await this.#loadLogsFromStorage();
  88    }
  89  
  90    // ============================================
  91    // Generic logging methods
  92    // ============================================
  93  
  94    log(value: any, data?: any) {
  95      this.#assureInitialized();
  96      this.#addLog('log', 'system', '📝', value, data);
  97      this.#consoleLog('log', value);
  98    }
  99  
 100    warn(value: any, data?: any) {
 101      this.#assureInitialized();
 102      this.#addLog('warn', 'system', '⚠️', value, data);
 103      this.#consoleLog('warn', value);
 104    }
 105  
 106    error(value: any, data?: any) {
 107      this.#assureInitialized();
 108      this.#addLog('error', 'system', '❌', value, data);
 109      this.#consoleLog('error', value);
 110    }
 111  
 112    debug(value: any, data?: any) {
 113      this.#assureInitialized();
 114      this.#addLog('debug', 'system', '🔍', value, data);
 115      this.#consoleLog('debug', value);
 116    }
 117  
 118    // ============================================
 119    // NIP-07 Action Logging
 120    // ============================================
 121  
 122    logNip07Action(
 123      method: string,
 124      host: string,
 125      approved: boolean,
 126      autoApproved: boolean,
 127      details?: { kind?: number; peerPubkey?: string }
 128    ) {
 129      this.#assureInitialized();
 130      const approvalType = autoApproved ? 'auto-approved' : approved ? 'approved' : 'denied';
 131      const icon = approved ? '✅' : '🚫';
 132  
 133      let message = `${method} from ${host} - ${approvalType}`;
 134      if (details?.kind !== undefined) {
 135        message += ` (kind: ${details.kind})`;
 136      }
 137  
 138      this.#addLog('log', 'nip07', icon, message, {
 139        method,
 140        host,
 141        approved,
 142        autoApproved,
 143        ...details,
 144      });
 145      this.#consoleLog('log', message);
 146    }
 147  
 148    logNip07GetPublicKey(host: string, approved: boolean, autoApproved: boolean) {
 149      this.logNip07Action('getPublicKey', host, approved, autoApproved);
 150    }
 151  
 152    logNip07SignEvent(
 153      host: string,
 154      kind: number,
 155      approved: boolean,
 156      autoApproved: boolean
 157    ) {
 158      this.logNip07Action('signEvent', host, approved, autoApproved, { kind });
 159    }
 160  
 161    logNip07Encrypt(
 162      method: 'nip04.encrypt' | 'nip44.encrypt',
 163      host: string,
 164      approved: boolean,
 165      autoApproved: boolean,
 166      peerPubkey?: string
 167    ) {
 168      this.logNip07Action(method, host, approved, autoApproved, { peerPubkey });
 169    }
 170  
 171    logNip07Decrypt(
 172      method: 'nip04.decrypt' | 'nip44.decrypt',
 173      host: string,
 174      approved: boolean,
 175      autoApproved: boolean,
 176      peerPubkey?: string
 177    ) {
 178      this.logNip07Action(method, host, approved, autoApproved, { peerPubkey });
 179    }
 180  
 181    logNip07GetRelays(host: string, approved: boolean, autoApproved: boolean) {
 182      this.logNip07Action('getRelays', host, approved, autoApproved);
 183    }
 184  
 185    // ============================================
 186    // Permission Logging
 187    // ============================================
 188  
 189    logPermissionStored(
 190      host: string,
 191      method: string,
 192      policy: string,
 193      kind?: number
 194    ) {
 195      this.#assureInitialized();
 196      const icon = policy === 'allow' ? '🔓' : '🔒';
 197      let message = `Permission stored: ${method} for ${host} - ${policy}`;
 198      if (kind !== undefined) {
 199        message += ` (kind: ${kind})`;
 200      }
 201      this.#addLog('log', 'permission', icon, message, { host, method, policy, kind });
 202      this.#consoleLog('log', message);
 203    }
 204  
 205    logPermissionDeleted(host: string, method: string, kind?: number) {
 206      this.#assureInitialized();
 207      let message = `Permission deleted: ${method} for ${host}`;
 208      if (kind !== undefined) {
 209        message += ` (kind: ${kind})`;
 210      }
 211      this.#addLog('log', 'permission', '🗑️', message, { host, method, kind });
 212      this.#consoleLog('log', message);
 213    }
 214  
 215    // ============================================
 216    // Vault Operations Logging
 217    // ============================================
 218  
 219    logVaultUnlock() {
 220      this.#assureInitialized();
 221      this.#addLog('log', 'vault', '🔓', 'Vault unlocked', undefined);
 222      this.#consoleLog('log', 'Vault unlocked');
 223    }
 224  
 225    logVaultLock() {
 226      this.#assureInitialized();
 227      this.#addLog('log', 'vault', '🔒', 'Vault locked', undefined);
 228      this.#consoleLog('log', 'Vault locked');
 229    }
 230  
 231    logVaultCreated() {
 232      this.#assureInitialized();
 233      this.#addLog('log', 'vault', '🆕', 'Vault created', undefined);
 234      this.#consoleLog('log', 'Vault created');
 235    }
 236  
 237    logVaultExport(fileName: string) {
 238      this.#assureInitialized();
 239      this.#addLog('log', 'vault', '📤', `Vault exported: ${fileName}`, { fileName });
 240      this.#consoleLog('log', `Vault exported: ${fileName}`);
 241    }
 242  
 243    logVaultImport(fileName: string) {
 244      this.#assureInitialized();
 245      this.#addLog('log', 'vault', '📥', `Vault imported: ${fileName}`, { fileName });
 246      this.#consoleLog('log', `Vault imported: ${fileName}`);
 247    }
 248  
 249    logVaultReset() {
 250      this.#assureInitialized();
 251      this.#addLog('warn', 'vault', '🗑️', 'Extension reset', undefined);
 252      this.#consoleLog('warn', 'Extension reset');
 253    }
 254  
 255    // ============================================
 256    // Profile Operations Logging
 257    // ============================================
 258  
 259    logProfileFetchError(pubkey: string, error: string) {
 260      this.#assureInitialized();
 261      const shortPubkey = pubkey.substring(0, 8) + '...';
 262      this.#addLog('error', 'profile', '👤', `Failed to fetch profile for ${shortPubkey}: ${error}`, {
 263        pubkey,
 264        error,
 265      });
 266      this.#consoleLog('error', `Failed to fetch profile for ${shortPubkey}: ${error}`);
 267    }
 268  
 269    logProfileParseError(pubkey: string) {
 270      this.#assureInitialized();
 271      const shortPubkey = pubkey.substring(0, 8) + '...';
 272      this.#addLog('error', 'profile', '👤', `Failed to parse profile content for ${shortPubkey}`, {
 273        pubkey,
 274      });
 275      this.#consoleLog('error', `Failed to parse profile content for ${shortPubkey}`);
 276    }
 277  
 278    logNip05ValidationError(nip05: string, error: string) {
 279      this.#assureInitialized();
 280      this.#addLog('error', 'profile', '🔗', `NIP-05 validation failed for ${nip05}: ${error}`, {
 281        nip05,
 282        error,
 283      });
 284      this.#consoleLog('error', `NIP-05 validation failed for ${nip05}: ${error}`);
 285    }
 286  
 287    logNip05ValidationSuccess(nip05: string, pubkey: string) {
 288      this.#assureInitialized();
 289      const shortPubkey = pubkey.substring(0, 8) + '...';
 290      this.#addLog('log', 'profile', '✓', `NIP-05 verified: ${nip05} → ${shortPubkey}`, {
 291        nip05,
 292        pubkey,
 293      });
 294      this.#consoleLog('log', `NIP-05 verified: ${nip05} → ${shortPubkey}`);
 295    }
 296  
 297    logProfileEdit(identityNick: string, field: string) {
 298      this.#assureInitialized();
 299      this.#addLog('log', 'profile', '✏️', `Profile edited: ${identityNick} - ${field}`, {
 300        identityNick,
 301        field,
 302      });
 303      this.#consoleLog('log', `Profile edited: ${identityNick} - ${field}`);
 304    }
 305  
 306    logIdentityCreated(nick: string) {
 307      this.#assureInitialized();
 308      this.#addLog('log', 'profile', '🆕', `Identity created: ${nick}`, { nick });
 309      this.#consoleLog('log', `Identity created: ${nick}`);
 310    }
 311  
 312    logIdentityDeleted(nick: string) {
 313      this.#assureInitialized();
 314      this.#addLog('warn', 'profile', '🗑️', `Identity deleted: ${nick}`, { nick });
 315      this.#consoleLog('warn', `Identity deleted: ${nick}`);
 316    }
 317  
 318    logIdentitySelected(nick: string) {
 319      this.#assureInitialized();
 320      this.#addLog('log', 'profile', '👆', `Identity selected: ${nick}`, { nick });
 321      this.#consoleLog('log', `Identity selected: ${nick}`);
 322    }
 323  
 324    // ============================================
 325    // Bookmark Operations Logging
 326    // ============================================
 327  
 328    logBookmarkAdded(url: string, title: string) {
 329      this.#assureInitialized();
 330      this.#addLog('log', 'bookmark', '🔖', `Bookmark added: ${title}`, { url, title });
 331      this.#consoleLog('log', `Bookmark added: ${title}`);
 332    }
 333  
 334    logBookmarkRemoved(url: string, title: string) {
 335      this.#assureInitialized();
 336      this.#addLog('log', 'bookmark', '🗑️', `Bookmark removed: ${title}`, { url, title });
 337      this.#consoleLog('log', `Bookmark removed: ${title}`);
 338    }
 339  
 340    // ============================================
 341    // System/Error Logging
 342    // ============================================
 343  
 344    logRelayFetchError(identityNick: string, error: string) {
 345      this.#assureInitialized();
 346      this.#addLog('error', 'system', '📡', `Failed to fetch relays for ${identityNick}: ${error}`, {
 347        identityNick,
 348        error,
 349      });
 350      this.#consoleLog('error', `Failed to fetch relays for ${identityNick}: ${error}`);
 351    }
 352  
 353    logStorageError(operation: string, error: string) {
 354      this.#assureInitialized();
 355      this.#addLog('error', 'system', '💾', `Storage error (${operation}): ${error}`, {
 356        operation,
 357        error,
 358      });
 359      this.#consoleLog('error', `Storage error (${operation}): ${error}`);
 360    }
 361  
 362    logCryptoError(operation: string, error: string) {
 363      this.#assureInitialized();
 364      this.#addLog('error', 'system', '🔐', `Crypto error (${operation}): ${error}`, {
 365        operation,
 366        error,
 367      });
 368      this.#consoleLog('error', `Crypto error (${operation}): ${error}`);
 369    }
 370  
 371    // ============================================
 372    // Internal methods
 373    // ============================================
 374  
 375    async clear(): Promise<void> {
 376      this.#logs = [];
 377      await this.#saveLogsToStorage();
 378    }
 379  
 380    #addLog(
 381      level: LogEntry['level'],
 382      category: LogCategory,
 383      icon: string,
 384      message: any,
 385      data?: any
 386    ) {
 387      const entry: LogEntry = {
 388        timestamp: new Date(),
 389        level,
 390        category,
 391        icon,
 392        message: typeof message === 'string' ? message : JSON.stringify(message),
 393        data,
 394      };
 395      this.#logs.unshift(entry);
 396  
 397      // Limit stored logs
 398      if (this.#logs.length > this.#maxLogs) {
 399        this.#logs.pop();
 400      }
 401  
 402      // Save to storage asynchronously (don't block)
 403      this.#saveLogsToStorage();
 404    }
 405  
 406    #consoleLog(_level: 'log' | 'warn' | 'error' | 'debug', _message: string) {
 407      // Logs stored in-memory + session storage; console output disabled to reduce noise.
 408    }
 409  
 410    #assureInitialized() {
 411      if (!this.#namespace) {
 412        throw new Error(
 413          'LoggerService not initialized. Please call initialize(..) first.'
 414        );
 415      }
 416    }
 417  }
 418  
 419  // ============================================
 420  // Standalone functions for background script
 421  // (Background script runs in different context without Angular DI)
 422  // ============================================
 423  
 424  export async function backgroundLog(
 425    category: LogCategory,
 426    icon: string,
 427    level: LogEntry['level'],
 428    message: string,
 429    data?: any
 430  ): Promise<void> {
 431    try {
 432      if (typeof chrome === 'undefined' || !chrome.storage?.session) {
 433        console.log(`[Background] ${message}`);
 434        return;
 435      }
 436  
 437      const result = await chrome.storage.session.get(LOGS_STORAGE_KEY);
 438      const existingLogs: StoredLogEntry[] = result[LOGS_STORAGE_KEY] || [];
 439  
 440      const newEntry: StoredLogEntry = {
 441        timestamp: new Date().toISOString(),
 442        level,
 443        category,
 444        icon,
 445        message,
 446        data,
 447      };
 448  
 449      const updatedLogs = [newEntry, ...existingLogs].slice(0, 500);
 450      await chrome.storage.session.set({ [LOGS_STORAGE_KEY]: updatedLogs });
 451    } catch (error) {
 452      console.error('Failed to add background log:', error);
 453    }
 454  }
 455  
 456  export async function backgroundLogNip07Action(
 457    method: string,
 458    host: string,
 459    approved: boolean,
 460    autoApproved: boolean,
 461    details?: { kind?: number; peerPubkey?: string }
 462  ): Promise<void> {
 463    const approvalType = autoApproved
 464      ? 'auto-approved'
 465      : approved
 466        ? 'approved'
 467        : 'denied';
 468    const icon = approved ? '✅' : '🚫';
 469  
 470    let message = `${method} from ${host} - ${approvalType}`;
 471    if (details?.kind !== undefined) {
 472      message += ` (kind: ${details.kind})`;
 473    }
 474  
 475    await backgroundLog('nip07', icon, 'log', message, {
 476      method,
 477      host,
 478      approved,
 479      autoApproved,
 480      ...details,
 481    });
 482  }
 483  
 484  export async function backgroundLogPermissionStored(
 485    host: string,
 486    method: string,
 487    policy: string,
 488    kind?: number
 489  ): Promise<void> {
 490    const icon = policy === 'allow' ? '🔓' : '🔒';
 491    let message = `Permission stored: ${method} for ${host} - ${policy}`;
 492    if (kind !== undefined) {
 493      message += ` (kind: ${kind})`;
 494    }
 495    await backgroundLog('permission', icon, 'log', message, {
 496      host,
 497      method,
 498      policy,
 499      kind,
 500    });
 501  }
 502