/** * Session manager for active user sessions */ import type { NostrEvent } from '../../types/nostr.js'; export type AuthMethod = 'nip07' | 'nsec' | 'anonymous'; export interface UserSession { pubkey: string; method: AuthMethod; signer: (event: Omit) => Promise; createdAt: number; // Store password in memory for nsec/anonymous sessions (never persisted) // This allows signing without re-prompting, but password is cleared on logout password?: string; // Only for 'nsec' and 'anonymous' methods } // Simple store implementation for Svelte reactivity function createStore(initial: T) { let value = initial; const subscribers = new Set<(value: T) => void>(); return { get value() { return value; }, set(newValue: T) { value = newValue; subscribers.forEach((fn) => fn(value)); }, subscribe(fn: (value: T) => void) { subscribers.add(fn); fn(value); return () => subscribers.delete(fn); } }; } class SessionManager { private currentSession: UserSession | null = null; public session = createStore(null); /** * Set current session */ setSession(session: UserSession, metadata?: Record): void { this.currentSession = session; this.session.set(session); // Store in localStorage for persistence // NEVER store password in localStorage - it's only kept in memory if (typeof window !== 'undefined') { const sessionData: any = { pubkey: session.pubkey, method: session.method, createdAt: session.createdAt }; // Store method-specific metadata for restoration if (metadata) { sessionData.metadata = metadata; } // Password is never persisted - only kept in memory localStorage.setItem('aitherboard_session', JSON.stringify(sessionData)); } } /** * Get current session */ getSession(): UserSession | null { return this.currentSession; } /** * Check if user is logged in */ isLoggedIn(): boolean { return this.currentSession !== null; } /** * Get current pubkey */ getCurrentPubkey(): string | null { return this.currentSession?.pubkey || null; } /** * Sign event with current session */ async signEvent(event: Omit): Promise { if (!this.currentSession) { throw new Error('No active session'); } return this.currentSession.signer(event); } /** * Clear session * Also clears password from memory for security */ clearSession(): void { // Clear password from memory if it exists if (this.currentSession?.password) { // Overwrite password in memory (though JS doesn't guarantee this) this.currentSession.password = ''; } this.currentSession = null; this.session.set(null); if (typeof window !== 'undefined') { localStorage.removeItem('aitherboard_session'); } } /** * Restore session from localStorage * This will attempt to restore the session based on the auth method * Only restores if there's no active session (to avoid overwriting sessions with passwords) */ async restoreSession(): Promise { if (typeof window === 'undefined') return false; // Don't restore if there's already an active session (especially one with a password) if (this.currentSession) { return true; // Session already exists, consider it restored } const stored = localStorage.getItem('aitherboard_session'); if (!stored) return false; try { const data = JSON.parse(stored); const { pubkey, method, metadata } = data; if (!pubkey || !method) return false; // Import auth handlers dynamically to avoid circular dependencies switch (method) { case 'nip07': { // For NIP-07, we can restore by checking if extension is still available const { isNIP07Available, getPublicKeyWithNIP07, signEventWithNIP07 } = await import('./nip07-signer.js'); if (isNIP07Available()) { try { // Verify the extension still has the same pubkey const currentPubkey = await getPublicKeyWithNIP07(); if (currentPubkey === pubkey) { this.setSession({ pubkey, method: 'nip07', signer: signEventWithNIP07, createdAt: data.createdAt || Date.now() }); return true; } } catch { // Extension error, can't restore return false; } } return false; } case 'anonymous': { // For anonymous, we can restore if the encrypted key is stored // The key is stored in IndexedDB, we just need to verify it exists // We can't restore without password, but we can check if key exists if (pubkey) { try { // Check if key exists by trying to list keys (we can't decrypt without password) // For now, restore session but signer will require password this.setSession({ pubkey, method: 'anonymous', signer: async () => { throw new Error('Anonymous session requires password. Please log in again.'); }, createdAt: data.createdAt || Date.now() }); // Note: This session won't work until user re-authenticates with password return true; } catch { return false; } } return false; } case 'nsec': { // For nsec, we can restore the session but signing will require password // The encrypted key is stored in IndexedDB, but we need password to decrypt // For now, restore session but signer will fail until user re-enters password if (pubkey) { try { const { hasNsecKey } = await import('../cache/nsec-key-store.js'); const keyExists = await hasNsecKey(pubkey); if (keyExists) { // Restore session but signer will require password this.setSession({ pubkey, method: 'nsec', signer: async () => { throw new Error('Nsec session requires password. Please log in again.'); }, createdAt: data.createdAt || Date.now() }); return true; } } catch { return false; } } // Clear the stored session if key doesn't exist localStorage.removeItem('aitherboard_session'); return false; } default: return false; } } catch (error) { console.error('Error restoring session:', error); // Clear corrupted session data localStorage.removeItem('aitherboard_session'); return false; } } } export const sessionManager = new SessionManager();