import { nostrService } from './nostr' import { keyManagementService } from './keyManagement' import type { NostrConnectState } from '@/types/nostr' /** * Nostr authentication service using local key management * Keys are stored encrypted in IndexedDB and decrypted using recovery phrase */ export class NostrAuthService { private state: NostrConnectState = { connected: false, pubkey: null, profile: null, } private listeners: Set<(state: NostrConnectState) => void> = new Set() private unlockedPrivateKey: string | null = null constructor() { if (typeof window !== 'undefined') { this.loadStateFromStorage() this.setupMessageListener() } } subscribe(callback: (state: NostrConnectState) => void): () => void { this.listeners.add(callback) callback(this.state) return () => { this.listeners.delete(callback) } } getState(): NostrConnectState { return { ...this.state } } /** * Check if account exists */ async accountExists(): Promise { return keyManagementService.accountExists() } /** * Create a new account (generate or import key) * Returns recovery phrase and npub */ async createAccount(privateKey?: string): Promise<{ recoveryPhrase: string[] npub: string publicKey: string }> { const result = await keyManagementService.createAccount(privateKey) // Set public key immediately this.state = { connected: false, pubkey: result.publicKey, profile: null, } nostrService.setPublicKey(result.publicKey) this.saveStateToStorage() this.notifyListeners() return result } /** * Unlock account using recovery phrase */ async unlockAccount(recoveryPhrase: string[]): Promise { try { const keys = await keyManagementService.unlockAccount(recoveryPhrase) this.unlockedPrivateKey = keys.privateKey this.state = { connected: true, pubkey: keys.publicKey, profile: null, } nostrService.setPublicKey(keys.publicKey) nostrService.setPrivateKey(keys.privateKey) this.saveStateToStorage() this.notifyListeners() void this.loadProfile() } catch (e) { console.error('Error unlocking account:', e) throw new Error(`Failed to unlock account: ${e instanceof Error ? e.message : 'Unknown error'}`) } } /** * Connect using existing stored keys (if already unlocked) * This is called when the app loads and keys are already available */ async connect(): Promise { // Check if account exists const exists = await keyManagementService.accountExists() if (!exists) { throw new Error('No account found. Please create an account first.') } // Try to get public keys const publicKeys = await keyManagementService.getPublicKeys() if (!publicKeys) { throw new Error('Account exists but public keys not found') } // Set public key but don't unlock private key yet // Private key will be unlocked when user provides recovery phrase this.state = { connected: false, pubkey: publicKeys.publicKey, profile: null, } nostrService.setPublicKey(publicKeys.publicKey) this.saveStateToStorage() this.notifyListeners() void this.loadProfile() } /** * Get the private key if unlocked */ getPrivateKey(): string | null { return this.unlockedPrivateKey } /** * Check if private key is unlocked */ isUnlocked(): boolean { return this.unlockedPrivateKey !== null } disconnect(): void { this.unlockedPrivateKey = null this.state = { connected: false, pubkey: null, profile: null, } // Clear keys from nostrService - the service stores keys internally, we just clear our reference // The service will continue to work but won't have access to the keys nostrService.setPrivateKey('') nostrService.setPublicKey('') this.saveStateToStorage() this.notifyListeners() } /** * Delete account (remove all stored keys) */ async deleteAccount(): Promise { await keyManagementService.deleteAccount() this.disconnect() } private async loadProfile(): Promise { if (!this.state.pubkey) { return } try { const profile = await nostrService.getProfile(this.state.pubkey) if (profile) { this.state.profile = profile this.saveStateToStorage() this.notifyListeners() } } catch (e) { console.error('Error loading profile:', e) } } private setupMessageListener(): void { window.addEventListener('storage', (e) => { if (e.key === 'nostr_auth_state') { this.loadStateFromStorage() } }) } private loadStateFromStorage(): void { try { const stored = localStorage.getItem('nostr_auth_state') if (stored) { const parsed = JSON.parse(stored) this.state = { connected: parsed.connected ?? false, pubkey: parsed.pubkey ?? null, profile: parsed.profile ?? null, } if (this.state.pubkey) { nostrService.setPublicKey(this.state.pubkey) } // Note: private key is not stored, it must be unlocked with recovery phrase } } catch (e) { console.error('Error loading state from storage:', e) } } private saveStateToStorage(): void { try { // Only save public information, never private keys const stateToSave = { connected: this.state.connected, pubkey: this.state.pubkey, profile: this.state.profile, } localStorage.setItem('nostr_auth_state', JSON.stringify(stateToSave)) } catch (e) { console.error('Error saving state to storage:', e) } } private notifyListeners(): void { this.listeners.forEach((callback) => callback({ ...this.state })) } } export const nostrAuthService = new NostrAuthService()