/** * Unified authentication handler */ import { getNIP07Signer, signEventWithNIP07, getPublicKeyWithNIP07 } from '../auth/nip07-signer.js'; import { signEventWithNsec, getPublicKeyFromNsec } from '../auth/nsec-signer.js'; import { signEventWithAnonymous, generateAnonymousKey, getStoredAnonymousKey } from '../auth/anonymous-signer.js'; import { decryptPrivateKey, encryptPrivateKey } from '../security/key-management.js'; import { sessionManager, type AuthMethod } from '../auth/session-manager.js'; import { fetchRelayLists, fetchProfile } from '../user-data.js'; import { nostrClient } from './nostr-client.js'; import { relayManager } from './relay-manager.js'; import type { NostrEvent } from '../../types/nostr.js'; import { KIND } from '../../types/kind-lookup.js'; // Mute list and blocked relays management const muteList: Set = new Set(); const blockedRelays: Set = new Set(); /** * Authenticate with NIP-07 */ export async function authenticateWithNIP07(): Promise { const pubkey = await getPublicKeyWithNIP07(); sessionManager.setSession({ pubkey, method: 'nip07', signer: signEventWithNIP07, createdAt: Date.now() }, {}); // No metadata needed for NIP-07 // Fetch user relay lists and mute list in background with timeout // Don't block login if relays are slow or unavailable Promise.race([ loadUserPreferences(pubkey), new Promise((resolve) => setTimeout(resolve, 5000)) // 5 second timeout ]).catch(() => { // Silently fail - preference loading errors shouldn't break login }); // Fetch and cache user's own profile (background-update if already cached) fetchProfile(pubkey).catch(() => { // Silently fail - profile fetch errors shouldn't break login }); return pubkey; } /** * Authenticate with existing stored nsec key (password only) * NEVER logs the password - it is sensitive */ export async function authenticateWithStoredNsec( pubkey: string, password: string ): Promise { // Verify the key exists and password is correct by trying to decrypt const { getNsecKey } = await import('../cache/nsec-key-store.js'); const decryptedNsec = await getNsecKey(pubkey, password); if (!decryptedNsec) { throw new Error('Invalid password or key not found'); } // Create session with signer that uses stored encrypted key // Store password in memory (never persisted) so signing works without re-prompting sessionManager.setSession({ pubkey, method: 'nsec', password, // Store in memory for signing - never persisted to localStorage signer: async (event) => { // Retrieve stored ncryptsec directly (no need to decrypt and re-encrypt) const session = sessionManager.getSession(); if (!session || !session.password) { throw new Error('Session password not available'); } const { getNcryptsec } = await import('../cache/nsec-key-store.js'); const ncryptsec = await getNcryptsec(pubkey); if (!ncryptsec) { throw new Error('Stored nsec key not found'); } // Use stored ncryptsec directly - it's already encrypted return signEventWithNsec(event, ncryptsec, session.password); }, createdAt: Date.now() }, { pubkey }); // Store pubkey in metadata for restoration (password never persisted) await loadUserPreferences(pubkey); // Fetch and cache user's own profile (background-update if already cached) fetchProfile(pubkey).catch(() => { // Silently fail - profile fetch errors shouldn't break login }); return pubkey; } /** * Authenticate with nsec (new key - will be stored) * NEVER logs the nsec, password, or ncryptsec - they are sensitive */ export async function authenticateWithNsec( nsec: string, password: string ): Promise { // Derive public key from private key - NEVER log the nsec const pubkey = await getPublicKeyFromNsec(nsec); // Encrypt and store the nsec key in IndexedDB // NEVER log the nsec or password const { storeNsecKey } = await import('../cache/nsec-key-store.js'); await storeNsecKey(nsec, password, pubkey); // Verify the key was stored correctly by trying to retrieve it // This ensures the key is committed to IndexedDB before we proceed const { getNsecKey } = await import('../cache/nsec-key-store.js'); try { const verifyNsec = await getNsecKey(pubkey, password); if (!verifyNsec) { throw new Error('Failed to retrieve stored nsec key'); } // Compare hex strings (case-insensitive) to handle any case differences const nsecLower = nsec.toLowerCase().trim(); const verifyLower = verifyNsec.toLowerCase().trim(); if (nsecLower !== verifyLower) { throw new Error('Stored nsec key does not match original - key may be corrupted'); } } catch (error) { // If verification fails, provide helpful error if (error instanceof Error) { throw error; } throw new Error('Failed to verify stored nsec key - please try again'); } // Create session with signer that uses stored encrypted key // Store password in memory (never persisted) so signing works without re-prompting // The signer will retrieve and decrypt when needed, but never log the key sessionManager.setSession({ pubkey, method: 'nsec', password, // Store in memory for signing - never persisted to localStorage signer: async (event) => { // Retrieve and decrypt key when signing - never log it const session = sessionManager.getSession(); if (!session || !session.password) { throw new Error('Session password not available'); } const { getNcryptsec } = await import('../cache/nsec-key-store.js'); try { // Get stored ncryptsec directly (no need to decrypt and re-encrypt) const ncryptsec = await getNcryptsec(pubkey); if (!ncryptsec) { throw new Error('Stored nsec key not found'); } // Use stored ncryptsec directly - it's already encrypted return signEventWithNsec(event, ncryptsec, session.password); } catch (error) { // Provide better error message without exposing sensitive data if (error instanceof Error) { // Re-throw with more context if it's a decryption error if (error.message.includes('decrypt') || error.message.includes('password')) { throw new Error('Failed to decrypt stored nsec key. Please log in again with your password.'); } throw error; } throw new Error('Failed to retrieve nsec key for signing'); } }, createdAt: Date.now() }, { pubkey }); // Store pubkey in metadata for restoration (password never persisted) await loadUserPreferences(pubkey); // Fetch and cache user's own profile (background-update if already cached) fetchProfile(pubkey).catch(() => { // Silently fail - profile fetch errors shouldn't break login }); return pubkey; } /** * Authenticate with existing stored anonymous key (password only) * NEVER logs the password - it is sensitive */ export async function authenticateWithStoredAnonymous( pubkey: string, password: string ): Promise { // Verify the key exists and password is correct by trying to decrypt const decryptedNsec = await getStoredAnonymousKey(pubkey, password); if (!decryptedNsec) { throw new Error('Invalid password or key not found'); } // Store password in memory (never persisted) so signing works without re-prompting // Create session with signer that retrieves and decrypts when needed sessionManager.setSession({ pubkey, method: 'anonymous', password, // Store in memory for signing - never persisted to localStorage signer: async (event) => { // Retrieve and decrypt key when signing - never log it const session = sessionManager.getSession(); if (!session || !session.password) { throw new Error('Session password not available'); } return signEventWithAnonymous(event, pubkey, session.password); }, createdAt: Date.now() }, { pubkey }); // Store pubkey in metadata for restoration (password never persisted) await loadUserPreferences(pubkey); // Fetch and cache user's own profile (background-update if already cached) fetchProfile(pubkey).catch(() => { // Silently fail - profile fetch errors shouldn't break login }); return pubkey; } /** * Authenticate as anonymous (new key - will be generated and stored) * Generates a new key, encrypts it, and stores it in IndexedDB * NEVER logs the generated nsec or password */ export async function authenticateAsAnonymous(password: string): Promise { // Generate new anonymous key - never log the nsec const { pubkey, nsec } = await generateAnonymousKey(password); // Key is already stored encrypted in IndexedDB by generateAnonymousKey // Store password in memory (never persisted) so signing works without re-prompting // Create session with signer that retrieves and decrypts when needed sessionManager.setSession({ pubkey, method: 'anonymous', password, // Store in memory for signing - never persisted to localStorage signer: async (event) => { // Retrieve and decrypt key when signing - never log it const session = sessionManager.getSession(); if (!session || !session.password) { throw new Error('Session password not available'); } return signEventWithAnonymous(event, pubkey, session.password); }, createdAt: Date.now() }, { pubkey }); // Store pubkey in metadata for restoration (password never persisted) await loadUserPreferences(pubkey); // Fetch and cache user's own profile (background-update if already cached) fetchProfile(pubkey).catch(() => { // Silently fail - profile fetch errors shouldn't break login }); return pubkey; } /** * Load user preferences (relay lists, mute list, blocked relays) */ async function loadUserPreferences(pubkey: string): Promise { try { // Fetch relay lists and load into relay manager with timeout await Promise.race([ relayManager.loadUserPreferences(pubkey), new Promise((_, reject) => setTimeout(() => reject(new Error('Relay list fetch timeout')), 5000) ) ]); } catch (error) { // If relay list fetch fails, continue with default relays console.debug('[auth-handler] Failed to load user relay preferences, using defaults'); } try { // Fetch mute list (kind 10000) with timeout const muteEvents = await Promise.race([ nostrClient.fetchEvents( [{ kinds: [KIND.MUTE_LIST], authors: [pubkey], limit: 1 }], relayManager.getProfileReadRelays(), { useCache: true, cacheResults: true, timeout: 5000 } ), new Promise((resolve) => setTimeout(() => resolve([]), 5000) ) ]); if (muteEvents.length > 0) { const mutedPubkeys = muteEvents[0].tags .filter((t) => t[0] === 'p') .map((t) => t[1]) .filter(Boolean) as string[]; muteList.clear(); mutedPubkeys.forEach(pk => muteList.add(pk)); } } catch (error) { // Silently fail - mute list fetch errors shouldn't break login console.debug('[auth-handler] Failed to load mute list'); } try { // Fetch blocked relays (kind 10006) with timeout const blockedRelayEvents = await Promise.race([ nostrClient.fetchEvents( [{ kinds: [KIND.BLOCKED_RELAYS], authors: [pubkey], limit: 1 }], relayManager.getProfileReadRelays(), { useCache: true, cacheResults: true, timeout: 5000 } ), new Promise((resolve) => setTimeout(() => resolve([]), 5000) ) ]); if (blockedRelayEvents.length > 0) { const blocked = blockedRelayEvents[0].tags .filter((t) => t[0] === 'relay') .map((t) => t[1]) .filter(Boolean) as string[]; blockedRelays.clear(); blocked.forEach(r => blockedRelays.add(r)); // Update relay manager with blocked relays relayManager.updateBlockedRelays(blockedRelays); } } catch (error) { // Silently fail - blocked relay fetch errors shouldn't break login console.debug('[auth-handler] Failed to load blocked relays'); } } /** * Sign HTTP auth (NIP-98) for authenticated HTTP requests * Returns Authorization header value: "Nostr " */ export async function signHttpAuth( url: string, method: string, description: string = '' ): Promise { const event = await sessionManager.signEvent({ kind: KIND.HTTP_AUTH, pubkey: sessionManager.getCurrentPubkey()!, created_at: Math.floor(Date.now() / 1000), tags: [ ['u', url], ['method', method] ], content: description }); // Base64 encode the event JSON and return as "Nostr " const eventJson = JSON.stringify(event); const base64 = btoa(eventJson); return `Nostr ${base64}`; } /** * Sign and publish event */ export async function signAndPublish( event: Omit, relays?: string[] ): Promise<{ success: string[]; failed: Array<{ relay: string; error: string }>; }> { const signed = await sessionManager.signEvent(event); return nostrClient.publish(signed, { relays }); } /** * Logout */ export function logout(): void { sessionManager.clearSession(); muteList.clear(); blockedRelays.clear(); relayManager.clearUserPreferences(); } /** * Get mute list */ export function getMuteList(): Set { return muteList; } /** * Get blocked relays */ export function getBlockedRelays(): Set { return blockedRelays; }