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