From 3109a93163427b97067fc5e8a7377e81fad91f62 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 22 Sep 2025 15:37:53 -0400 Subject: [PATCH] Fixed issue not recognizing browser extension --- examples/keytest.html | 12 + lite/VERSION | 2 +- lite/build.js | 831 +++++++++++++++++----------------------- lite/nostr-lite.js | 870 ++++++++++++++++++------------------------ lite/ui/modal.js | 76 ++-- 5 files changed, 779 insertions(+), 1012 deletions(-) diff --git a/examples/keytest.html b/examples/keytest.html index ed7005a..5820fd8 100644 --- a/examples/keytest.html +++ b/examples/keytest.html @@ -104,6 +104,18 @@ }}); + // Check for existing authentication state on page load + const authState = getAuthState(); + if (authState && authState.method) { + console.log('Found existing authentication:', authState.method); + document.getElementById('status').textContent = `Authenticated with: ${authState.method}`; + document.getElementById('test-section').style.display = 'block'; + + // Store some test data for encryption/decryption + window.testCiphertext = null; + window.testCiphertext44 = null; + } + // Listen for authentication events window.addEventListener('nlMethodSelected', (event) => { console.log('User authenticated:', event.detail); diff --git a/lite/VERSION b/lite/VERSION index 845639e..9faa1b7 100644 --- a/lite/VERSION +++ b/lite/VERSION @@ -1 +1 @@ -0.1.4 +0.1.5 diff --git a/lite/build.js b/lite/build.js index af19f01..7599f72 100644 --- a/lite/build.js +++ b/lite/build.js @@ -109,7 +109,7 @@ if (typeof window !== 'undefined') { bundle += ` style.id = 'nl-theme-css';\n`; bundle += ` style.textContent = themeCss;\n`; bundle += ` document.head.appendChild(style);\n`; - bundle += ` console.log(\`NOSTR_LOGIN_LITE: \${themeName} theme CSS injected\`);\n`; + bundle += ` console.log('NOSTR_LOGIN_LITE: ' + themeName + ' theme CSS injected');\n`; bundle += ` }\n`; bundle += `}\n\n`; @@ -131,24 +131,46 @@ if (typeof window !== 'undefined') { let modalContent = fs.readFileSync(modalPath, 'utf8'); - // Read version from VERSION file and inject into modal title + // Read version from VERSION file for bottom-right display const versionPath = path.join(__dirname, 'VERSION'); - let versionTitle = 'Nostr Login'; + let versionString = ''; if (fs.existsSync(versionPath)) { try { const version = fs.readFileSync(versionPath, 'utf8').trim(); - versionTitle = `Nostr Login v${version}`; - console.log(`šŸ”¢ Using version: ${version}`); + versionString = 'v' + version; + console.log('šŸ”¢ Using version: ' + version); } catch (error) { - console.warn('āš ļø Could not read VERSION file, using default title'); + console.warn('āš ļø Could not read VERSION file, no version will be displayed'); } } else { - console.log('šŸ“‹ No VERSION file found, using default title'); + console.log('šŸ“‹ No VERSION file found, no version will be displayed'); } - // Replace the modal title in the content - modalContent = modalContent.replace(/modalTitle\.textContent = 'Nostr Login';/, `modalTitle.textContent = '${versionTitle}';`); + // Keep modal title as just "Nostr Login" (no version injection) + // Add version element in bottom-right corner if version exists + if (versionString) { + // Find the modalContent.appendChild(this.modalBody); line and insert version element before it + modalContent = modalContent.replace( + /modalContent\.appendChild\(this\.modalBody\);/, + `// Add version element in bottom-right corner aligned with modal body + const versionElement = document.createElement('div'); + versionElement.textContent = '${versionString}'; + versionElement.style.cssText = \` + position: absolute; + bottom: 8px; + right: 24px; + font-size: 14px; + color: #666666; + font-family: var(--nl-font-family, 'Courier New', monospace); + pointer-events: none; + z-index: 1; + \`; + modalContent.appendChild(versionElement); + + modalContent.appendChild(this.modalBody);` + ); + } // Skip header comments let lines = modalContent.split('\n'); @@ -555,9 +577,9 @@ class FloatingTab { // Use profile name if available, otherwise pubkey if (this.userProfile?.name || this.userProfile?.display_name) { const userName = this.userProfile.name || this.userProfile.display_name; - userDisplay = userName.length > 16 ? \`\${userName.slice(0, 16)}...\` : userName; + userDisplay = userName.length > 16 ? userName.slice(0, 16) + '...' : userName; } else { - userDisplay = \`\${authState.pubkey.slice(0, 8)}...\${authState.pubkey.slice(-4)}\`; + userDisplay = authState.pubkey.slice(0, 8) + '...' + authState.pubkey.slice(-4); } } else { userDisplay = 'Authenticated'; @@ -608,7 +630,7 @@ class FloatingTab { // Fallback to pubkey display display = this.options.appearance.iconOnly ? authState.pubkey.slice(0, 6) - : \`\${authState.pubkey.slice(0, 6)}...\`; + : authState.pubkey.slice(0, 6) + '...'; } else { display = this.options.appearance.iconOnly ? 'User' : 'Authenticated'; } @@ -618,7 +640,7 @@ class FloatingTab { } else { const display = this.options.appearance.iconOnly ? this.options.appearance.icon : - (this.options.appearance.icon ? \`\${this.options.appearance.icon} \${this.options.appearance.text}\` : this.options.appearance.text); + (this.options.appearance.icon ? this.options.appearance.icon + ' ' + this.options.appearance.text : this.options.appearance.text); this.container.textContent = display; this.container.className = 'nl-floating-tab nl-floating-tab--logged-out'; @@ -732,10 +754,10 @@ class FloatingTab { const x = this.options.hPosition * (window.innerWidth - this.container.offsetWidth - padding * 2) + padding + this.options.offset.x; const y = this.options.vPosition * (window.innerHeight - this.container.offsetHeight - padding * 2) + padding + this.options.offset.y; - this.container.style.left = \`\${x}px\`; - this.container.style.top = \`\${y}px\`; + this.container.style.left = x + 'px'; + this.container.style.top = y + 'px'; - console.log(\`FloatingTab: Positioned at (\${x}, \${y})\`); + console.log('FloatingTab: Positioned at (' + x + ', ' + y + ')'); } _slideIn() { @@ -964,16 +986,37 @@ class NostrLite { this._installFacade(window.nostr); // Install facade with any existing nostr object console.log('šŸ” NOSTR_LOGIN_LITE: āœ… Facade installed for local/NIP-46/readonly methods'); + + // CRITICAL FIX: Immediately attempt to restore auth state after facade installation + if (this.facadeInstalled && window.nostr?.restoreAuthState) { + console.log('šŸ” NOSTR_LOGIN_LITE: šŸ”„ IMMEDIATELY attempting auth restoration after facade installation'); + try { + const restoredAuth = await window.nostr.restoreAuthState(); + if (restoredAuth) { + console.log('šŸ” NOSTR_LOGIN_LITE: āœ… Auth state restored immediately during facade setup!'); + console.log('šŸ” NOSTR_LOGIN_LITE: Method:', restoredAuth.method); + console.log('šŸ” NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); + + // Update facade's authState immediately + window.nostr.authState = restoredAuth; + } else { + console.log('šŸ” NOSTR_LOGIN_LITE: āŒ No auth state to restore during facade setup'); + } + } catch (error) { + console.error('šŸ” NOSTR_LOGIN_LITE: āŒ Error restoring auth during facade setup:', error); + } + } } } - _installFacade(existingNostr = null) { - if (typeof window !== 'undefined' && !this.facadeInstalled) { + _installFacade(existingNostr = null, forceInstall = false) { + if (typeof window !== 'undefined' && (!this.facadeInstalled || forceInstall)) { console.log('šŸ” NOSTR_LOGIN_LITE: === _installFacade CALLED ==='); console.log('šŸ” NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr); console.log('šŸ” NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name); + console.log('šŸ” NOSTR_LOGIN_LITE: forceInstall flag:', forceInstall); const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession }); window.nostr = facade; @@ -983,6 +1026,8 @@ class NostrLite { console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name); console.log('šŸ” NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr); + } else if (typeof window !== 'undefined') { + console.log('šŸ” NOSTR_LOGIN_LITE: _installFacade skipped - facadeInstalled:', this.facadeInstalled, 'forceInstall:', forceInstall); } } @@ -1081,6 +1126,13 @@ class NostrLite { console.log('šŸ” NOSTR_LOGIN_LITE: Method:', restoredAuth.method); console.log('šŸ” NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); + // CRITICAL FIX: Activate facade resilience system for non-extension methods + // Extensions like nos2x can override our facade after page refresh + if (restoredAuth.method === 'local' || restoredAuth.method === 'nip46') { + console.log('šŸ” NOSTR_LOGIN_LITE: šŸ›”ļø Activating facade resilience system for page refresh'); + this._activateResilienceProtection(restoredAuth.method); + } + // Handle NIP-46 reconnection requirement if (restoredAuth.requiresReconnection) { console.log('šŸ” NOSTR_LOGIN_LITE: NIP-46 connection requires user reconnection'); @@ -1107,6 +1159,45 @@ class NostrLite { } } + // Activate facade resilience protection against extension overrides + _activateResilienceProtection(method) { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: === ACTIVATING RESILIENCE PROTECTION ==='); + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Protecting facade for method:', method); + + // Store the current extension if any (for potential restoration later) + const preservedExtension = this.preservedExtension || + ((window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null); + + // DELAYED FACADE RESILIENCE - Reinstall after extension override attempts + const forceReinstallFacade = () => { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name); + + // If facade was overridden by extension, reinstall it + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method); + this._installFacade(preservedExtension, true); + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name); + + // Schedule another check in case of persistent extension override + setTimeout(() => { + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method); + this._installFacade(preservedExtension, true); + } + }, 1000); + } else { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Facade persistence verified - no override detected'); + } + }; + + // Schedule resilience checks at multiple intervals (same as Modal) + setTimeout(forceReinstallFacade, 100); // Quick check + setTimeout(forceReinstallFacade, 500); // Main check + setTimeout(forceReinstallFacade, 1500); // Final check + + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Resilience protection scheduled for method:', method); + } + // Extension-specific authentication restoration async _attemptExtensionRestore() { try { @@ -1207,7 +1298,7 @@ class NostrLite { // CSS-only theme switching switchTheme(themeName) { - console.log(\`NOSTR_LOGIN_LITE: Switching to \${themeName} theme\`); + console.log('NOSTR_LOGIN_LITE: Switching to ' + themeName + ' theme'); if (THEME_CSS[themeName]) { injectThemeCSS(themeName); @@ -1222,7 +1313,7 @@ class NostrLite { return { theme: themeName }; } else { - console.warn(\`Theme '\${themeName}' not found, using default\`); + console.warn("Theme '" + themeName + "' not found, using default"); injectThemeCSS('default'); this.currentTheme = 'default'; return { theme: 'default' }; @@ -1290,115 +1381,10 @@ class NostrLite { } // ====================================== -// Authentication Manager for Persistent Login +// Simplified Authentication Manager (Unified Plaintext Storage) // ====================================== -// Encryption utilities for secure local storage -class CryptoUtils { - static async generateKey() { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - return await window.crypto.subtle.generateKey( - { - name: 'AES-GCM', - length: 256, - }, - true, - ['encrypt', 'decrypt'] - ); - } - - static async deriveKey(password, salt) { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - const encoder = new TextEncoder(); - const keyMaterial = await window.crypto.subtle.importKey( - 'raw', - encoder.encode(password), - { name: 'PBKDF2' }, - false, - ['deriveBits', 'deriveKey'] - ); - - return await window.crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt: salt, - iterations: 100000, - hash: 'SHA-256', - }, - keyMaterial, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - } - - static async encrypt(data, key) { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - const encoder = new TextEncoder(); - const iv = window.crypto.getRandomValues(new Uint8Array(12)); - - const encrypted = await window.crypto.subtle.encrypt( - { - name: 'AES-GCM', - iv: iv, - }, - key, - encoder.encode(data) - ); - - return { - encrypted: new Uint8Array(encrypted), - iv: iv - }; - } - - static async decrypt(encryptedData, key, iv) { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - const decrypted = await window.crypto.subtle.decrypt( - { - name: 'AES-GCM', - iv: iv, - }, - key, - encryptedData - ); - - const decoder = new TextDecoder(); - return decoder.decode(decrypted); - } - - static arrayBufferToBase64(buffer) { - const bytes = new Uint8Array(buffer); - let binary = ''; - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); - } - - static base64ToArrayBuffer(base64) { - const binary = window.atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes.buffer; - } -} - -// Unified authentication state manager +// Simple authentication state manager - plaintext storage for maximum usability class AuthManager { constructor(options = {}) { this.storageKey = 'nostr_login_lite_auth'; @@ -1407,16 +1393,22 @@ class AuthManager { // Configure storage type based on isolateSession option if (options.isolateSession) { this.storage = sessionStorage; - console.log('AuthManager: Using sessionStorage for per-window isolation'); + console.log('šŸ” AuthManager: Using sessionStorage for per-window isolation'); } else { this.storage = localStorage; - console.log('AuthManager: Using localStorage for cross-window persistence'); + console.log('šŸ” AuthManager: Using localStorage for cross-window persistence'); } + + console.warn('šŸ” SECURITY: Private keys stored unencrypted in browser storage'); + console.warn('šŸ” For production apps, implement your own secure storage'); } - // Save authentication state with method-specific security + // Save authentication state using unified plaintext approach async saveAuthState(authData) { try { + console.log('šŸ” AuthManager: Saving auth state with plaintext storage'); + console.warn('šŸ” SECURITY: Private key will be stored unencrypted for maximum usability'); + const authState = { method: authData.method, timestamp: Date.now(), @@ -1431,24 +1423,15 @@ class AuthManager { hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function', hasSignEvent: typeof authData.extension?.signEvent === 'function' }; + console.log('šŸ” AuthManager: Extension method - storing verification data only'); break; case 'local': - // For local keys, encrypt the secret key + // UNIFIED PLAINTEXT: Store secret key directly for maximum compatibility if (authData.secret) { - const password = this._generateSessionPassword(); - const salt = window.crypto.getRandomValues(new Uint8Array(16)); - const key = await CryptoUtils.deriveKey(password, salt); - const encrypted = await CryptoUtils.encrypt(authData.secret, key); - - authState.encrypted = { - data: CryptoUtils.arrayBufferToBase64(encrypted.encrypted), - iv: CryptoUtils.arrayBufferToBase64(encrypted.iv), - salt: CryptoUtils.arrayBufferToBase64(salt) - }; - - // Store session password in sessionStorage (cleared on tab close) - sessionStorage.setItem('nostr_session_key', password); + authState.secret = authData.secret; + console.log('šŸ” AuthManager: Local method - storing secret key in plaintext'); + console.warn('šŸ” SECURITY: Secret key stored unencrypted for developer convenience'); } break; @@ -1460,23 +1443,25 @@ class AuthManager { relays: authData.signer.relays, // Don't store secret - user will need to reconnect }; + console.log('šŸ” AuthManager: NIP-46 method - storing connection parameters'); } break; case 'readonly': // Read-only mode has no secrets to store + console.log('šŸ” AuthManager: Read-only method - storing basic auth state'); break; default: - throw new Error(\`Unknown auth method: \${authData.method}\`); + throw new Error('Unknown auth method: ' + authData.method); } this.storage.setItem(this.storageKey, JSON.stringify(authState)); this.currentAuthState = authState; - console.log('AuthManager: Auth state saved for method:', authData.method); + console.log('šŸ” AuthManager: Auth state saved successfully for method:', authData.method); } catch (error) { - console.error('AuthManager: Failed to save auth state:', error); + console.error('šŸ” AuthManager: Failed to save auth state:', error); throw error; } } @@ -1488,7 +1473,7 @@ class AuthManager { console.log('šŸ” AuthManager: storageKey:', this.storageKey); const stored = this.storage.getItem(this.storageKey); - console.log('šŸ” AuthManager: localStorage raw value:', stored); + console.log('šŸ” AuthManager: Storage raw value:', stored); if (!stored) { console.log('šŸ” AuthManager: āŒ No stored auth state found'); @@ -1711,51 +1696,57 @@ class AuthManager { } async _restoreLocalAuth(authState) { - if (!authState.encrypted) { - console.log('AuthManager: No encrypted data found for local auth'); - return null; - } - - // Get session password - const sessionPassword = sessionStorage.getItem('nostr_session_key'); - if (!sessionPassword) { - console.log('AuthManager: Session password not found, cannot decrypt'); - return null; - } - - try { - // Decrypt the secret key - const salt = CryptoUtils.base64ToArrayBuffer(authState.encrypted.salt); - const key = await CryptoUtils.deriveKey(sessionPassword, new Uint8Array(salt)); + console.log('šŸ” AuthManager: === _restoreLocalAuth (Unified Plaintext) ==='); + + // Check for legacy encrypted format first + if (authState.encrypted) { + console.log('šŸ” AuthManager: Detected LEGACY encrypted format - migrating to plaintext'); + console.warn('šŸ” SECURITY: Converting from encrypted to plaintext storage for compatibility'); - const encryptedData = CryptoUtils.base64ToArrayBuffer(authState.encrypted.data); - const iv = CryptoUtils.base64ToArrayBuffer(authState.encrypted.iv); - - const secret = await CryptoUtils.decrypt(encryptedData, key, new Uint8Array(iv)); + // Try to decrypt legacy format + const sessionPassword = sessionStorage.getItem('nostr_session_key'); + if (!sessionPassword) { + console.log('šŸ” AuthManager: Legacy session password not found - user must re-login'); + return null; + } - console.log('AuthManager: Local auth restored successfully'); - return { - method: 'local', - pubkey: authState.pubkey, - secret: secret - }; - - } catch (error) { - console.error('AuthManager: Failed to decrypt local key:', error); + try { + console.warn('šŸ” AuthManager: Legacy encryption system no longer supported - user must re-login'); + this.clearAuthState(); // Clear legacy format + return null; + } catch (error) { + console.error('šŸ” AuthManager: Legacy decryption failed:', error); + this.clearAuthState(); // Clear corrupted legacy format + return null; + } + } + + // NEW UNIFIED PLAINTEXT FORMAT + if (!authState.secret) { + console.log('šŸ” AuthManager: No secret found in plaintext format'); return null; } + + console.log('šŸ” AuthManager: āœ… Local auth restored from plaintext storage'); + console.warn('šŸ” SECURITY: Secret key was stored unencrypted'); + + return { + method: 'local', + pubkey: authState.pubkey, + secret: authState.secret + }; } async _restoreNip46Auth(authState) { if (!authState.nip46) { - console.log('AuthManager: No NIP-46 data found'); + console.log('šŸ” AuthManager: No NIP-46 data found'); return null; } // For NIP-46, we can't automatically restore the connection // because it requires the user to re-authenticate with the remote signer // Instead, we return the connection parameters so the UI can prompt for reconnection - console.log('AuthManager: NIP-46 connection data found, requires user reconnection'); + console.log('šŸ” AuthManager: NIP-46 connection data found, requires user reconnection'); return { method: 'nip46', pubkey: authState.pubkey, @@ -1765,7 +1756,7 @@ class AuthManager { } async _restoreReadonlyAuth(authState) { - console.log('AuthManager: Read-only auth restored successfully'); + console.log('šŸ” AuthManager: Read-only auth restored successfully'); return { method: 'readonly', pubkey: authState.pubkey @@ -1775,16 +1766,9 @@ class AuthManager { // Clear stored authentication state clearAuthState() { this.storage.removeItem(this.storageKey); - sessionStorage.removeItem('nostr_session_key'); + sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key this.currentAuthState = null; - console.log('AuthManager: Auth state cleared'); - } - - // Generate a session-specific password for local key encryption - _generateSessionPassword() { - const array = new Uint8Array(32); - window.crypto.getRandomValues(array); - return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); + console.log('šŸ” AuthManager: Auth state cleared from unified storage'); } // Check if we have valid stored auth @@ -1807,119 +1791,178 @@ class AuthManager { } } +// ====================================== +// Global Authentication Functions (Single Source of Truth) +// ====================================== + +// Global authentication state (single source of truth) +let globalAuthState = null; +let globalAuthManager = null; + +// Initialize global auth manager (lazy initialization) +function getGlobalAuthManager() { + if (!globalAuthManager) { + // Default to localStorage for persistence across browser sessions + globalAuthManager = new AuthManager({ isolateSession: false }); + } + return globalAuthManager; +} + +// **UNIFIED GLOBAL FUNCTION**: Set authentication state (works for all methods) +function setAuthState(authData, options = {}) { + try { + console.log('🌐 setAuthState: Setting global auth state for method:', authData.method); + console.warn('šŸ” SECURITY: Using unified plaintext storage for maximum compatibility'); + + // Store in memory + globalAuthState = authData; + + // Store in browser storage using AuthManager + const authManager = new AuthManager(options); + authManager.saveAuthState(authData); + + console.log('🌐 setAuthState: Auth state saved successfully'); + } catch (error) { + console.error('🌐 setAuthState: Failed to save auth state:', error); + throw error; + } +} + +// **UNIFIED GLOBAL FUNCTION**: Get authentication state (single source of truth) +function getAuthState() { + try { + // Always query from storage as the authoritative source + const authManager = getGlobalAuthManager(); + const storageKey = 'nostr_login_lite_auth'; + + // Check both session and local storage for compatibility + let stored = null; + if (sessionStorage.getItem(storageKey)) { + stored = sessionStorage.getItem(storageKey); + } else if (localStorage.getItem(storageKey)) { + stored = localStorage.getItem(storageKey); + } + + if (!stored) { + console.log('🌐 getAuthState: No auth state found in storage'); + globalAuthState = null; + return null; + } + + const authState = JSON.parse(stored); + console.log('🌐 getAuthState: Retrieved auth state:', authState.method); + + // Update in-memory cache + globalAuthState = authState; + return authState; + + } catch (error) { + console.error('🌐 getAuthState: Failed to get auth state:', error); + globalAuthState = null; + return null; + } +} + +// **UNIFIED GLOBAL FUNCTION**: Clear authentication state (works for all methods) +function clearAuthState() { + try { + console.log('🌐 clearAuthState: Clearing global auth state'); + + // Clear in-memory state + globalAuthState = null; + + // Clear from both storage types for thorough cleanup + const storageKey = 'nostr_login_lite_auth'; + localStorage.removeItem(storageKey); + sessionStorage.removeItem(storageKey); + sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key + + console.log('🌐 clearAuthState: Auth state cleared from all storage locations'); + } catch (error) { + console.error('🌐 clearAuthState: Failed to clear auth state:', error); + } +} + // NIP-07 compliant window.nostr provider class WindowNostr { - constructor(nostrLite, existingNostr = null) { + constructor(nostrLite, existingNostr = null, options = {}) { this.nostrLite = nostrLite; this.authState = null; this.existingNostr = existingNostr; this.authenticatedExtension = null; - this.authManager = new AuthManager({ isolateSession: nostrLite.options?.isolateSession }); + this.options = options; this._setupEventListeners(); } + // Restore authentication state on page load + async restoreAuthState() { + console.log('šŸ” WindowNostr: === restoreAuthState ==='); + + try { + // Use simplified AuthManager for consistent restore logic + const authManager = new AuthManager(this.options); + const restoredAuth = await authManager.restoreAuthState(); + + if (restoredAuth) { + console.log('šŸ” WindowNostr: āœ… Auth state restored:', restoredAuth.method); + this.authState = restoredAuth; + + // Update global state + globalAuthState = restoredAuth; + + // Dispatch restoration event + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('nlAuthRestored', { + detail: restoredAuth + })); + } + + return restoredAuth; + } else { + console.log('šŸ” WindowNostr: āŒ No auth state to restore'); + return null; + } + + } catch (error) { + console.error('šŸ” WindowNostr: Auth restoration failed:', error); + return null; + } + } + _setupEventListeners() { // Listen for authentication events to store auth state if (typeof window !== 'undefined') { window.addEventListener('nlMethodSelected', async (event) => { + console.log('šŸ” WindowNostr: nlMethodSelected event received:', event.detail); this.authState = event.detail; // If extension method, capture the specific extension the user chose if (event.detail.method === 'extension') { this.authenticatedExtension = event.detail.extension; - console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); + console.log('šŸ” WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); } - // Use global setAuthState function for unified persistence + // Use unified global setAuthState function for all methods try { - setAuthState(event.detail, { isolateSession: this.nostrLite.options?.isolateSession }); - console.log('WindowNostr: Auth state saved via global setAuthState'); + setAuthState(event.detail, this.options); + console.log('šŸ” WindowNostr: Auth state saved via unified setAuthState'); } catch (error) { - console.error('WindowNostr: Failed to save auth state via global setAuthState:', error); + console.error('šŸ” WindowNostr: Failed to save auth state:', error); } - - // EXTENSION-FIRST: Only reinstall facade for non-extension methods - // Extensions handle their own window.nostr - don't interfere! - if (event.detail.method !== 'extension' && typeof window !== 'undefined') { - console.log('WindowNostr: Re-installing facade after', this.authState?.method, 'authentication'); - window.nostr = this; - } else if (event.detail.method === 'extension') { - console.log('WindowNostr: Extension authentication - NOT reinstalling facade'); - } - - console.log('WindowNostr: Auth state updated:', this.authState?.method); }); window.addEventListener('nlLogout', () => { + console.log('šŸ” WindowNostr: nlLogout event received'); this.authState = null; this.authenticatedExtension = null; - // Clear persistent auth state - this.authManager.clearAuthState(); - console.log('WindowNostr: Auth state cleared and persistence removed'); - - // EXTENSION-FIRST: Only reinstall facade if we're not in extension mode - if (typeof window !== 'undefined' && !this.nostrLite?.hasExtension) { - console.log('WindowNostr: Re-installing facade after logout (non-extension mode)'); - window.nostr = this; - } else { - console.log('WindowNostr: Logout in extension mode - NOT reinstalling facade'); - } + // Clear from unified storage + clearAuthState(); + console.log('šŸ” WindowNostr: Auth state cleared via unified clearAuthState'); }); } } - // Restore authentication state on page load - async restoreAuthState() { - try { - console.log('šŸ” WindowNostr: === restoreAuthState START ==='); - console.log('šŸ” WindowNostr: authManager available:', !!this.authManager); - - const restoredAuth = await this.authManager.restoreAuthState(); - console.log('šŸ” WindowNostr: authManager.restoreAuthState result:', restoredAuth); - - if (restoredAuth) { - console.log('šŸ” WindowNostr: āœ… Setting authState to restored auth'); - this.authState = restoredAuth; - console.log('šŸ” WindowNostr: this.authState now:', this.authState); - - // Handle method-specific restoration - if (restoredAuth.method === 'extension') { - console.log('šŸ” WindowNostr: Extension method - setting authenticatedExtension'); - this.authenticatedExtension = restoredAuth.extension; - console.log('šŸ” WindowNostr: authenticatedExtension set to:', this.authenticatedExtension); - } - - console.log('šŸ” WindowNostr: āœ… Authentication state restored successfully!'); - console.log('šŸ” WindowNostr: Method:', restoredAuth.method); - console.log('šŸ” WindowNostr: Pubkey:', restoredAuth.pubkey); - - // Dispatch restoration event so UI can update - if (typeof window !== 'undefined') { - console.log('šŸ” WindowNostr: Dispatching nlAuthRestored event...'); - const event = new CustomEvent('nlAuthRestored', { - detail: restoredAuth - }); - console.log('šŸ” WindowNostr: Event detail:', event.detail); - window.dispatchEvent(event); - console.log('šŸ” WindowNostr: āœ… nlAuthRestored event dispatched'); - } - - console.log('šŸ” WindowNostr: === restoreAuthState END (success) ==='); - return restoredAuth; - } else { - console.log('šŸ” WindowNostr: āŒ No authentication state to restore (null from authManager)'); - console.log('šŸ” WindowNostr: === restoreAuthState END (no restore) ==='); - return null; - } - } catch (error) { - console.error('šŸ” WindowNostr: āŒ Failed to restore auth state:', error); - console.error('šŸ” WindowNostr: Error stack:', error.stack); - console.log('šŸ” WindowNostr: === restoreAuthState END (error) ==='); - return null; - } - } - async getPublicKey() { if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); @@ -1940,7 +1983,7 @@ class WindowNostr { throw new Error('Read-only mode - cannot get public key'); default: - throw new Error(\`Unsupported auth method: \${this.authState.method}\`); + throw new Error('Unsupported auth method: ' + this.authState.method); } } @@ -1956,14 +1999,7 @@ class WindowNostr { switch (this.authState.method) { case 'extension': // Use the captured authenticated extension, not current window.nostr - console.log('WindowNostr: signEvent - authenticatedExtension:', this.authenticatedExtension); - console.log('WindowNostr: signEvent - authState.extension:', this.authState.extension); - console.log('WindowNostr: signEvent - existingNostr:', this.existingNostr); - const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; - console.log('WindowNostr: signEvent - using extension:', ext); - console.log('WindowNostr: signEvent - extension constructor:', ext?.constructor?.name); - if (!ext) throw new Error('Extension not available'); return await ext.signEvent(event); @@ -1992,13 +2028,13 @@ class WindowNostr { } default: - throw new Error(\`Unsupported auth method: \${this.authState.method}\`); + throw new Error('Unsupported auth method: ' + this.authState.method); } } async getRelays() { - // Return default relays since we removed the relays configuration - return ['wss://relay.damus.io', 'wss://nos.lol']; + // Return configured relays from nostr-lite options + return this.nostrLite.options?.relays || ['wss://relay.damus.io']; } get nip04() { @@ -2041,7 +2077,7 @@ class WindowNostr { } default: - throw new Error(\`Unsupported auth method: \${this.authState.method}\`); + throw new Error('Unsupported auth method: ' + this.authState.method); } }, @@ -2083,7 +2119,7 @@ class WindowNostr { } default: - throw new Error(\`Unsupported auth method: \${this.authState.method}\`); + throw new Error('Unsupported auth method: ' + this.authState.method); } } }; @@ -2092,18 +2128,17 @@ class WindowNostr { get nip44() { return { encrypt: async (pubkey, plaintext) => { - const authState = getAuthState(); - if (!authState) { + if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } - if (authState.method === 'readonly') { + if (this.authState.method === 'readonly') { throw new Error('Read-only mode - cannot encrypt'); } - switch (authState.method) { + switch (this.authState.method) { case 'extension': { - const ext = this.authenticatedExtension || authState.extension || this.existingNostr; + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip44.encrypt(pubkey, plaintext); } @@ -2112,41 +2147,40 @@ class WindowNostr { const { nip44, nip19 } = window.NostrTools; let secretKey; - if (authState.secret.startsWith('nsec')) { - const decoded = nip19.decode(authState.secret); + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); secretKey = decoded.data; } else { - secretKey = this._hexToUint8Array(authState.secret); + secretKey = this._hexToUint8Array(this.authState.secret); } return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { - if (!authState.signer?.bunkerSigner) { + if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } - return await authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext); + return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext); } default: - throw new Error('Unsupported auth method: ' + authState.method); + throw new Error('Unsupported auth method: ' + this.authState.method); } }, decrypt: async (pubkey, ciphertext) => { - const authState = getAuthState(); - if (!authState) { + if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } - if (authState.method === 'readonly') { + if (this.authState.method === 'readonly') { throw new Error('Read-only mode - cannot decrypt'); } - switch (authState.method) { + switch (this.authState.method) { case 'extension': { - const ext = this.authenticatedExtension || authState.extension || this.existingNostr; + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip44.decrypt(pubkey, ciphertext); } @@ -2155,25 +2189,25 @@ class WindowNostr { const { nip44, nip19 } = window.NostrTools; let secretKey; - if (authState.secret.startsWith('nsec')) { - const decoded = nip19.decode(authState.secret); + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); secretKey = decoded.data; } else { - secretKey = this._hexToUint8Array(authState.secret); + secretKey = this._hexToUint8Array(this.authState.secret); } return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { - if (!authState.signer?.bunkerSigner) { + if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } - return await authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext); + return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext); } default: - throw new Error('Unsupported auth method: ' + authState.method); + throw new Error('Unsupported auth method: ' + this.authState.method); } } }; @@ -2191,171 +2225,6 @@ class WindowNostr { } } -// ====================================== -// Global Authentication State Manager - Single Source of Truth -// ====================================== - -// Storage-based authentication state - works regardless of extension presence -function getAuthState() { - try { - console.log('šŸ” getAuthState: === GLOBAL AUTH STATE CHECK ==='); - - const storageKey = 'nostr_login_lite_auth'; - let stored = null; - let storageType = null; - - // Check sessionStorage first (per-window isolation), then localStorage - if (sessionStorage.getItem(storageKey)) { - stored = sessionStorage.getItem(storageKey); - storageType = 'sessionStorage'; - console.log('šŸ” getAuthState: Found auth in sessionStorage'); - } else if (localStorage.getItem(storageKey)) { - stored = localStorage.getItem(storageKey); - storageType = 'localStorage'; - console.log('šŸ” getAuthState: Found auth in localStorage'); - } - - if (!stored) { - console.log('šŸ” getAuthState: āŒ No stored auth state found'); - return null; - } - - const authState = JSON.parse(stored); - console.log('šŸ” getAuthState: āœ… Parsed stored auth state from', storageType); - console.log('šŸ” getAuthState: Method:', authState.method); - console.log('šŸ” getAuthState: Pubkey:', authState.pubkey); - console.log('šŸ” getAuthState: Age (ms):', Date.now() - authState.timestamp); - - // Check if auth state is expired - const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000; - if (Date.now() - authState.timestamp > maxAge) { - console.log('šŸ” getAuthState: āŒ Auth state expired, clearing'); - sessionStorage.removeItem(storageKey); - localStorage.removeItem(storageKey); - return null; - } - - console.log('šŸ” getAuthState: āœ… Valid auth state found'); - return authState; - - } catch (error) { - console.error('šŸ” getAuthState: āŒ Error reading auth state:', error); - return null; - } -} - -// ====================================== -// Global Authentication State Management - Unified Persistence -// ====================================== - -// Global setAuthState function for unified persistence across all authentication methods -function setAuthState(authData, options = {}) { - try { - console.log('šŸ” setAuthState: === GLOBAL AUTH STATE SAVE ==='); - console.log('šŸ” setAuthState: authData:', authData); - console.log('šŸ” setAuthState: options:', options); - - const storageKey = 'nostr_login_lite_auth'; - - // Determine which storage to use based on isolateSession option - const storage = options.isolateSession ? sessionStorage : localStorage; - const storageType = options.isolateSession ? 'sessionStorage' : 'localStorage'; - - console.log('šŸ” setAuthState: Using', storageType, 'for persistence'); - - // Create auth state object - const authState = { - method: authData.method, - timestamp: Date.now(), - pubkey: authData.pubkey - }; - - // Add method-specific data (but no secrets for extension method) - switch (authData.method) { - case 'extension': - // For extensions, only store verification data - no secrets - authState.extensionVerification = { - constructor: authData.extension?.constructor?.name, - hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function', - hasSignEvent: typeof authData.extension?.signEvent === 'function' - }; - console.log('šŸ” setAuthState: Extension method - storing verification data only'); - break; - - case 'local': - // For local keys, store the secret (will be encrypted by AuthManager if needed) - if (authData.secret) { - authState.secret = authData.secret; - console.log('šŸ” setAuthState: Local method - storing secret key'); - } - break; - - case 'nip46': - // For NIP-46, store connection parameters - if (authData.signer) { - authState.nip46 = { - remotePubkey: authData.signer.remotePubkey, - relays: authData.signer.relays, - // Don't store secret - user will need to reconnect - }; - console.log('šŸ” setAuthState: NIP-46 method - storing connection parameters'); - } - break; - - case 'readonly': - // Read-only mode has no additional data to store - console.log('šŸ” setAuthState: Read-only method - storing basic auth state'); - break; - - default: - console.warn('šŸ” setAuthState: Unknown auth method:', authData.method); - break; - } - - // Store the auth state - storage.setItem(storageKey, JSON.stringify(authState)); - console.log('šŸ” setAuthState: āœ… Auth state saved successfully'); - console.log('šŸ” setAuthState: Final auth state:', authState); - - return authState; - - } catch (error) { - console.error('šŸ” setAuthState: āŒ Error saving auth state:', error); - throw error; - } -} - -// ====================================== -// Global Authentication State Clearing -// ====================================== - -// Global clearAuthState function for unified auth state clearing -function clearAuthState() { - try { - console.log('šŸ” clearAuthState: === GLOBAL AUTH STATE CLEAR ==='); - - const storageKey = 'nostr_login_lite_auth'; - - // Clear from both storage types to ensure complete cleanup - if (typeof sessionStorage !== 'undefined') { - sessionStorage.removeItem(storageKey); - sessionStorage.removeItem('nostr_session_key'); - console.log('šŸ” clearAuthState: āœ… Cleared auth state from sessionStorage'); - } - - if (typeof localStorage !== 'undefined') { - localStorage.removeItem(storageKey); - console.log('šŸ” clearAuthState: āœ… Cleared auth state from localStorage'); - } - - console.log('šŸ” clearAuthState: āœ… All auth state cleared successfully'); - - } catch (error) { - console.error('šŸ” clearAuthState: āŒ Error clearing auth state:', error); - } -} - - // Initialize and export if (typeof window !== 'undefined') { const nostrLite = new NostrLite(); @@ -2381,9 +2250,9 @@ if (typeof window !== 'undefined') { updateFloatingTab: (options) => nostrLite.updateFloatingTab(options), getFloatingTabState: () => nostrLite.getFloatingTabState(), - // GLOBAL AUTHENTICATION STATE API - Single Source of Truth - getAuthState: getAuthState, + // Global authentication state management (single source of truth) setAuthState: setAuthState, + getAuthState: getAuthState, clearAuthState: clearAuthState, // Expose for debugging @@ -2394,19 +2263,21 @@ if (typeof window !== 'undefined') { console.log('NOSTR_LOGIN_LITE: Library loaded and ready'); console.log('NOSTR_LOGIN_LITE: Use window.NOSTR_LOGIN_LITE.init(options) to initialize'); console.log('NOSTR_LOGIN_LITE: Detected', nostrLite.extensionBridge.getExtensionCount(), 'browser extensions'); + console.warn('šŸ” SECURITY: Unified plaintext storage enabled for maximum developer usability'); } else { // Node.js environment module.exports = { NostrLite }; } + `; // Write the complete bundle fs.writeFileSync(outputPath, bundle, 'utf8'); const sizeKB = (bundle.length / 1024).toFixed(2); - console.log(`\nāœ… nostr-lite.js bundle created: ${outputPath}`); - console.log(`šŸ“ Bundle size: ${sizeKB} KB`); - console.log(`šŸ“„ Total lines: ${bundle.split('\n').length}`); + console.log('\nāœ… nostr-lite.js bundle created: ' + outputPath); + console.log('šŸ“ Bundle size: ' + sizeKB + ' KB'); + console.log('šŸ“„ Total lines: ' + bundle.split('\n').length); // Check what's included const hasModal = bundle.includes('class Modal'); @@ -2414,15 +2285,15 @@ if (typeof window !== 'undefined') { const hasThemeCss = bundle.includes('THEME_CSS'); console.log('\nšŸ“‹ Bundle contents:'); - console.log(` Modal UI: ${hasModal ? 'āœ… Included' : 'āŒ Missing'}`); - console.log(` NOSTR_LOGIN_LITE: ${hasNostrLite ? 'āœ… Included' : 'āŒ Missing'}`); - console.log(` CSS-Only Themes: ${hasThemeCss ? 'āœ… Included' : 'āŒ Missing'}`); - console.log(` Extension Bridge: āœ… Included`); - console.log(` Window.nostr facade: āœ… Included`); + console.log(' Modal UI: ' + (hasModal ? 'āœ… Included' : 'āŒ Missing')); + console.log(' NOSTR_LOGIN_LITE: ' + (hasNostrLite ? 'āœ… Included' : 'āŒ Missing')); + console.log(' CSS-Only Themes: ' + (hasThemeCss ? 'āœ… Included' : 'āŒ Missing')); + console.log(' Extension Bridge: āœ… Included'); + console.log(' Window.nostr facade: āœ… Included'); console.log('\nšŸ“‹ Two-file architecture:'); console.log(' 1. nostr.bundle.js (official nostr-tools - 220KB)'); - console.log(` 2. nostr-lite.js (NOSTR_LOGIN_LITE with CSS-only themes - ${sizeKB}KB)`); + console.log(' 2. nostr-lite.js (NOSTR_LOGIN_LITE with CSS-only themes - ' + sizeKB + 'KB)'); return bundle; } diff --git a/lite/nostr-lite.js b/lite/nostr-lite.js index 9dc515b..d86ac61 100644 --- a/lite/nostr-lite.js +++ b/lite/nostr-lite.js @@ -8,7 +8,7 @@ * Two-file architecture: * 1. Load nostr.bundle.js (official nostr-tools bundle) * 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes) - * Generated on: 2025-09-21T15:51:33.328Z + * Generated on: 2025-09-22T19:37:53.923Z */ // Verify dependencies are loaded @@ -282,7 +282,7 @@ function injectThemeCSS(themeName = 'default') { style.id = 'nl-theme-css'; style.textContent = themeCss; document.head.appendChild(style); - console.log(`NOSTR_LOGIN_LITE: ${themeName} theme CSS injected`); + console.log('NOSTR_LOGIN_LITE: ' + themeName + ' theme CSS injected'); } } @@ -381,7 +381,7 @@ class Modal { `; const modalTitle = document.createElement('h2'); - modalTitle.textContent = 'Nostr Login v0.1.4'; + modalTitle.textContent = 'Nostr Login'; modalTitle.style.cssText = ` margin: 0; font-size: 24px; @@ -434,6 +434,21 @@ class Modal { `; modalContent.appendChild(modalHeader); + // Add version element in bottom-right corner aligned with modal body + const versionElement = document.createElement('div'); + versionElement.textContent = 'v0.1.5'; + versionElement.style.cssText = ` + position: absolute; + bottom: 8px; + right: 24px; + font-size: 14px; + color: #666666; + font-family: var(--nl-font-family, 'Courier New', monospace); + pointer-events: none; + z-index: 1; + `; + modalContent.appendChild(versionElement); + modalContent.appendChild(this.modalBody); this.container.appendChild(modalContent); @@ -1428,7 +1443,6 @@ class Modal { } _setAuthMethod(method, options = {}) { - // SINGLE-EXTENSION ARCHITECTURE: Handle method switching console.log('Modal: _setAuthMethod called with:', method, options); // CRITICAL: Never install facade for extension methods - leave window.nostr as the extension @@ -1451,46 +1465,57 @@ class Modal { return; } - // For non-extension methods, we need to ensure WindowNostr facade is available - console.log('Modal: Non-extension method detected:', method); + // FOR NON-EXTENSION METHODS: Force-install facade with resilience + console.log('Modal: Non-extension method - FORCE-INSTALLING facade with resilience:', method); - // Check if we have a preserved extension but no WindowNostr facade installed - const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension; - const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr'; + // Store the current extension if any (for potential restoration later) + const currentExtension = (window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null; - console.log('Modal: Method switching check:'); - console.log(' method:', method); - console.log(' hasPreservedExtension:', hasPreservedExtension); - console.log(' hasWindowNostrFacade:', hasWindowNostrFacade); - console.log(' current window.nostr constructor:', window.nostr?.constructor?.name); + // Get NostrLite instance for facade operations + const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; + if (!nostrLiteInstance || typeof nostrLiteInstance._installFacade !== 'function') { + console.error('Modal: Cannot access NostrLite instance or _installFacade method'); + // Fallback: emit event anyway + const event = new CustomEvent('nlMethodSelected', { + detail: { method, ...options } + }); + window.dispatchEvent(event); + this.close(); + return; + } - // If we have a preserved extension but no facade, install facade for method switching - if (hasPreservedExtension && !hasWindowNostrFacade) { - console.log('Modal: Installing WindowNostr facade for method switching (non-extension authentication)'); + // IMMEDIATE FACADE INSTALLATION + console.log('Modal: Installing WindowNostr facade immediately for method:', method); + const preservedExtension = nostrLiteInstance.preservedExtension || currentExtension; + nostrLiteInstance._installFacade(preservedExtension, true); + console.log('Modal: WindowNostr facade force-installed, current window.nostr:', window.nostr?.constructor?.name); + + // DELAYED FACADE RESILIENCE - Reinstall after extension override attempts + const forceReinstallFacade = () => { + console.log('Modal: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name); - // Get the NostrLite instance and install facade with preserved extension - const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; - if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') { - const preservedExtension = nostrLiteInstance.preservedExtension; - console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name); + // If facade was overridden by extension, reinstall it + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('Modal: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method); + nostrLiteInstance._installFacade(preservedExtension, true); + console.log('Modal: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name); - nostrLiteInstance._installFacade(preservedExtension); - console.log('Modal: WindowNostr facade installed for method switching'); + // Schedule another check in case of persistent extension override + setTimeout(() => { + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('Modal: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method); + nostrLiteInstance._installFacade(preservedExtension, true); + } + }, 1000); } else { - console.error('Modal: Cannot access NostrLite instance or _installFacade method'); + console.log('Modal: Facade persistence verified - no override detected'); } - } + }; - // If no extension at all, ensure facade is installed for local/NIP-46/readonly methods - else if (!hasPreservedExtension && !hasWindowNostrFacade) { - console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)'); - - const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; - if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') { - nostrLiteInstance._installFacade(); - console.log('Modal: WindowNostr facade installed for non-extension methods'); - } - } + // Schedule resilience checks at multiple intervals + setTimeout(forceReinstallFacade, 100); // Quick check + setTimeout(forceReinstallFacade, 500); // Main check + setTimeout(forceReinstallFacade, 1500); // Final check // Emit auth method selection const event = new CustomEvent('nlMethodSelected', { @@ -2563,9 +2588,9 @@ class FloatingTab { // Use profile name if available, otherwise pubkey if (this.userProfile?.name || this.userProfile?.display_name) { const userName = this.userProfile.name || this.userProfile.display_name; - userDisplay = userName.length > 16 ? `${userName.slice(0, 16)}...` : userName; + userDisplay = userName.length > 16 ? userName.slice(0, 16) + '...' : userName; } else { - userDisplay = `${authState.pubkey.slice(0, 8)}...${authState.pubkey.slice(-4)}`; + userDisplay = authState.pubkey.slice(0, 8) + '...' + authState.pubkey.slice(-4); } } else { userDisplay = 'Authenticated'; @@ -2616,7 +2641,7 @@ class FloatingTab { // Fallback to pubkey display display = this.options.appearance.iconOnly ? authState.pubkey.slice(0, 6) - : `${authState.pubkey.slice(0, 6)}...`; + : authState.pubkey.slice(0, 6) + '...'; } else { display = this.options.appearance.iconOnly ? 'User' : 'Authenticated'; } @@ -2626,7 +2651,7 @@ class FloatingTab { } else { const display = this.options.appearance.iconOnly ? this.options.appearance.icon : - (this.options.appearance.icon ? `${this.options.appearance.icon} ${this.options.appearance.text}` : this.options.appearance.text); + (this.options.appearance.icon ? this.options.appearance.icon + ' ' + this.options.appearance.text : this.options.appearance.text); this.container.textContent = display; this.container.className = 'nl-floating-tab nl-floating-tab--logged-out'; @@ -2740,10 +2765,10 @@ class FloatingTab { const x = this.options.hPosition * (window.innerWidth - this.container.offsetWidth - padding * 2) + padding + this.options.offset.x; const y = this.options.vPosition * (window.innerHeight - this.container.offsetHeight - padding * 2) + padding + this.options.offset.y; - this.container.style.left = `${x}px`; - this.container.style.top = `${y}px`; + this.container.style.left = x + 'px'; + this.container.style.top = y + 'px'; - console.log(`FloatingTab: Positioned at (${x}, ${y})`); + console.log('FloatingTab: Positioned at (' + x + ', ' + y + ')'); } _slideIn() { @@ -2972,16 +2997,37 @@ class NostrLite { this._installFacade(window.nostr); // Install facade with any existing nostr object console.log('šŸ” NOSTR_LOGIN_LITE: āœ… Facade installed for local/NIP-46/readonly methods'); + + // CRITICAL FIX: Immediately attempt to restore auth state after facade installation + if (this.facadeInstalled && window.nostr?.restoreAuthState) { + console.log('šŸ” NOSTR_LOGIN_LITE: šŸ”„ IMMEDIATELY attempting auth restoration after facade installation'); + try { + const restoredAuth = await window.nostr.restoreAuthState(); + if (restoredAuth) { + console.log('šŸ” NOSTR_LOGIN_LITE: āœ… Auth state restored immediately during facade setup!'); + console.log('šŸ” NOSTR_LOGIN_LITE: Method:', restoredAuth.method); + console.log('šŸ” NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); + + // Update facade's authState immediately + window.nostr.authState = restoredAuth; + } else { + console.log('šŸ” NOSTR_LOGIN_LITE: āŒ No auth state to restore during facade setup'); + } + } catch (error) { + console.error('šŸ” NOSTR_LOGIN_LITE: āŒ Error restoring auth during facade setup:', error); + } + } } } - _installFacade(existingNostr = null) { - if (typeof window !== 'undefined' && !this.facadeInstalled) { + _installFacade(existingNostr = null, forceInstall = false) { + if (typeof window !== 'undefined' && (!this.facadeInstalled || forceInstall)) { console.log('šŸ” NOSTR_LOGIN_LITE: === _installFacade CALLED ==='); console.log('šŸ” NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr); console.log('šŸ” NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name); + console.log('šŸ” NOSTR_LOGIN_LITE: forceInstall flag:', forceInstall); const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession }); window.nostr = facade; @@ -2991,6 +3037,8 @@ class NostrLite { console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name); console.log('šŸ” NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr); + } else if (typeof window !== 'undefined') { + console.log('šŸ” NOSTR_LOGIN_LITE: _installFacade skipped - facadeInstalled:', this.facadeInstalled, 'forceInstall:', forceInstall); } } @@ -3089,6 +3137,13 @@ class NostrLite { console.log('šŸ” NOSTR_LOGIN_LITE: Method:', restoredAuth.method); console.log('šŸ” NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); + // CRITICAL FIX: Activate facade resilience system for non-extension methods + // Extensions like nos2x can override our facade after page refresh + if (restoredAuth.method === 'local' || restoredAuth.method === 'nip46') { + console.log('šŸ” NOSTR_LOGIN_LITE: šŸ›”ļø Activating facade resilience system for page refresh'); + this._activateResilienceProtection(restoredAuth.method); + } + // Handle NIP-46 reconnection requirement if (restoredAuth.requiresReconnection) { console.log('šŸ” NOSTR_LOGIN_LITE: NIP-46 connection requires user reconnection'); @@ -3115,6 +3170,45 @@ class NostrLite { } } + // Activate facade resilience protection against extension overrides + _activateResilienceProtection(method) { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: === ACTIVATING RESILIENCE PROTECTION ==='); + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Protecting facade for method:', method); + + // Store the current extension if any (for potential restoration later) + const preservedExtension = this.preservedExtension || + ((window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null); + + // DELAYED FACADE RESILIENCE - Reinstall after extension override attempts + const forceReinstallFacade = () => { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name); + + // If facade was overridden by extension, reinstall it + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method); + this._installFacade(preservedExtension, true); + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name); + + // Schedule another check in case of persistent extension override + setTimeout(() => { + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method); + this._installFacade(preservedExtension, true); + } + }, 1000); + } else { + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Facade persistence verified - no override detected'); + } + }; + + // Schedule resilience checks at multiple intervals (same as Modal) + setTimeout(forceReinstallFacade, 100); // Quick check + setTimeout(forceReinstallFacade, 500); // Main check + setTimeout(forceReinstallFacade, 1500); // Final check + + console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Resilience protection scheduled for method:', method); + } + // Extension-specific authentication restoration async _attemptExtensionRestore() { try { @@ -3215,7 +3309,7 @@ class NostrLite { // CSS-only theme switching switchTheme(themeName) { - console.log(`NOSTR_LOGIN_LITE: Switching to ${themeName} theme`); + console.log('NOSTR_LOGIN_LITE: Switching to ' + themeName + ' theme'); if (THEME_CSS[themeName]) { injectThemeCSS(themeName); @@ -3230,7 +3324,7 @@ class NostrLite { return { theme: themeName }; } else { - console.warn(`Theme '${themeName}' not found, using default`); + console.warn("Theme '" + themeName + "' not found, using default"); injectThemeCSS('default'); this.currentTheme = 'default'; return { theme: 'default' }; @@ -3298,115 +3392,10 @@ class NostrLite { } // ====================================== -// Authentication Manager for Persistent Login +// Simplified Authentication Manager (Unified Plaintext Storage) // ====================================== -// Encryption utilities for secure local storage -class CryptoUtils { - static async generateKey() { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - return await window.crypto.subtle.generateKey( - { - name: 'AES-GCM', - length: 256, - }, - true, - ['encrypt', 'decrypt'] - ); - } - - static async deriveKey(password, salt) { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - const encoder = new TextEncoder(); - const keyMaterial = await window.crypto.subtle.importKey( - 'raw', - encoder.encode(password), - { name: 'PBKDF2' }, - false, - ['deriveBits', 'deriveKey'] - ); - - return await window.crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt: salt, - iterations: 100000, - hash: 'SHA-256', - }, - keyMaterial, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - } - - static async encrypt(data, key) { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - const encoder = new TextEncoder(); - const iv = window.crypto.getRandomValues(new Uint8Array(12)); - - const encrypted = await window.crypto.subtle.encrypt( - { - name: 'AES-GCM', - iv: iv, - }, - key, - encoder.encode(data) - ); - - return { - encrypted: new Uint8Array(encrypted), - iv: iv - }; - } - - static async decrypt(encryptedData, key, iv) { - if (!window.crypto?.subtle) { - throw new Error('Web Crypto API not available'); - } - - const decrypted = await window.crypto.subtle.decrypt( - { - name: 'AES-GCM', - iv: iv, - }, - key, - encryptedData - ); - - const decoder = new TextDecoder(); - return decoder.decode(decrypted); - } - - static arrayBufferToBase64(buffer) { - const bytes = new Uint8Array(buffer); - let binary = ''; - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); - } - - static base64ToArrayBuffer(base64) { - const binary = window.atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes.buffer; - } -} - -// Unified authentication state manager +// Simple authentication state manager - plaintext storage for maximum usability class AuthManager { constructor(options = {}) { this.storageKey = 'nostr_login_lite_auth'; @@ -3415,16 +3404,22 @@ class AuthManager { // Configure storage type based on isolateSession option if (options.isolateSession) { this.storage = sessionStorage; - console.log('AuthManager: Using sessionStorage for per-window isolation'); + console.log('šŸ” AuthManager: Using sessionStorage for per-window isolation'); } else { this.storage = localStorage; - console.log('AuthManager: Using localStorage for cross-window persistence'); + console.log('šŸ” AuthManager: Using localStorage for cross-window persistence'); } + + console.warn('šŸ” SECURITY: Private keys stored unencrypted in browser storage'); + console.warn('šŸ” For production apps, implement your own secure storage'); } - // Save authentication state with method-specific security + // Save authentication state using unified plaintext approach async saveAuthState(authData) { try { + console.log('šŸ” AuthManager: Saving auth state with plaintext storage'); + console.warn('šŸ” SECURITY: Private key will be stored unencrypted for maximum usability'); + const authState = { method: authData.method, timestamp: Date.now(), @@ -3439,24 +3434,15 @@ class AuthManager { hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function', hasSignEvent: typeof authData.extension?.signEvent === 'function' }; + console.log('šŸ” AuthManager: Extension method - storing verification data only'); break; case 'local': - // For local keys, encrypt the secret key + // UNIFIED PLAINTEXT: Store secret key directly for maximum compatibility if (authData.secret) { - const password = this._generateSessionPassword(); - const salt = window.crypto.getRandomValues(new Uint8Array(16)); - const key = await CryptoUtils.deriveKey(password, salt); - const encrypted = await CryptoUtils.encrypt(authData.secret, key); - - authState.encrypted = { - data: CryptoUtils.arrayBufferToBase64(encrypted.encrypted), - iv: CryptoUtils.arrayBufferToBase64(encrypted.iv), - salt: CryptoUtils.arrayBufferToBase64(salt) - }; - - // Store session password in sessionStorage (cleared on tab close) - sessionStorage.setItem('nostr_session_key', password); + authState.secret = authData.secret; + console.log('šŸ” AuthManager: Local method - storing secret key in plaintext'); + console.warn('šŸ” SECURITY: Secret key stored unencrypted for developer convenience'); } break; @@ -3468,23 +3454,25 @@ class AuthManager { relays: authData.signer.relays, // Don't store secret - user will need to reconnect }; + console.log('šŸ” AuthManager: NIP-46 method - storing connection parameters'); } break; case 'readonly': // Read-only mode has no secrets to store + console.log('šŸ” AuthManager: Read-only method - storing basic auth state'); break; default: - throw new Error(`Unknown auth method: ${authData.method}`); + throw new Error('Unknown auth method: ' + authData.method); } this.storage.setItem(this.storageKey, JSON.stringify(authState)); this.currentAuthState = authState; - console.log('AuthManager: Auth state saved for method:', authData.method); + console.log('šŸ” AuthManager: Auth state saved successfully for method:', authData.method); } catch (error) { - console.error('AuthManager: Failed to save auth state:', error); + console.error('šŸ” AuthManager: Failed to save auth state:', error); throw error; } } @@ -3496,7 +3484,7 @@ class AuthManager { console.log('šŸ” AuthManager: storageKey:', this.storageKey); const stored = this.storage.getItem(this.storageKey); - console.log('šŸ” AuthManager: localStorage raw value:', stored); + console.log('šŸ” AuthManager: Storage raw value:', stored); if (!stored) { console.log('šŸ” AuthManager: āŒ No stored auth state found'); @@ -3719,51 +3707,57 @@ class AuthManager { } async _restoreLocalAuth(authState) { - if (!authState.encrypted) { - console.log('AuthManager: No encrypted data found for local auth'); - return null; - } - - // Get session password - const sessionPassword = sessionStorage.getItem('nostr_session_key'); - if (!sessionPassword) { - console.log('AuthManager: Session password not found, cannot decrypt'); - return null; - } - - try { - // Decrypt the secret key - const salt = CryptoUtils.base64ToArrayBuffer(authState.encrypted.salt); - const key = await CryptoUtils.deriveKey(sessionPassword, new Uint8Array(salt)); + console.log('šŸ” AuthManager: === _restoreLocalAuth (Unified Plaintext) ==='); + + // Check for legacy encrypted format first + if (authState.encrypted) { + console.log('šŸ” AuthManager: Detected LEGACY encrypted format - migrating to plaintext'); + console.warn('šŸ” SECURITY: Converting from encrypted to plaintext storage for compatibility'); - const encryptedData = CryptoUtils.base64ToArrayBuffer(authState.encrypted.data); - const iv = CryptoUtils.base64ToArrayBuffer(authState.encrypted.iv); - - const secret = await CryptoUtils.decrypt(encryptedData, key, new Uint8Array(iv)); + // Try to decrypt legacy format + const sessionPassword = sessionStorage.getItem('nostr_session_key'); + if (!sessionPassword) { + console.log('šŸ” AuthManager: Legacy session password not found - user must re-login'); + return null; + } - console.log('AuthManager: Local auth restored successfully'); - return { - method: 'local', - pubkey: authState.pubkey, - secret: secret - }; - - } catch (error) { - console.error('AuthManager: Failed to decrypt local key:', error); + try { + console.warn('šŸ” AuthManager: Legacy encryption system no longer supported - user must re-login'); + this.clearAuthState(); // Clear legacy format + return null; + } catch (error) { + console.error('šŸ” AuthManager: Legacy decryption failed:', error); + this.clearAuthState(); // Clear corrupted legacy format + return null; + } + } + + // NEW UNIFIED PLAINTEXT FORMAT + if (!authState.secret) { + console.log('šŸ” AuthManager: No secret found in plaintext format'); return null; } + + console.log('šŸ” AuthManager: āœ… Local auth restored from plaintext storage'); + console.warn('šŸ” SECURITY: Secret key was stored unencrypted'); + + return { + method: 'local', + pubkey: authState.pubkey, + secret: authState.secret + }; } async _restoreNip46Auth(authState) { if (!authState.nip46) { - console.log('AuthManager: No NIP-46 data found'); + console.log('šŸ” AuthManager: No NIP-46 data found'); return null; } // For NIP-46, we can't automatically restore the connection // because it requires the user to re-authenticate with the remote signer // Instead, we return the connection parameters so the UI can prompt for reconnection - console.log('AuthManager: NIP-46 connection data found, requires user reconnection'); + console.log('šŸ” AuthManager: NIP-46 connection data found, requires user reconnection'); return { method: 'nip46', pubkey: authState.pubkey, @@ -3773,7 +3767,7 @@ class AuthManager { } async _restoreReadonlyAuth(authState) { - console.log('AuthManager: Read-only auth restored successfully'); + console.log('šŸ” AuthManager: Read-only auth restored successfully'); return { method: 'readonly', pubkey: authState.pubkey @@ -3783,16 +3777,9 @@ class AuthManager { // Clear stored authentication state clearAuthState() { this.storage.removeItem(this.storageKey); - sessionStorage.removeItem('nostr_session_key'); + sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key this.currentAuthState = null; - console.log('AuthManager: Auth state cleared'); - } - - // Generate a session-specific password for local key encryption - _generateSessionPassword() { - const array = new Uint8Array(32); - window.crypto.getRandomValues(array); - return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); + console.log('šŸ” AuthManager: Auth state cleared from unified storage'); } // Check if we have valid stored auth @@ -3815,119 +3802,178 @@ class AuthManager { } } +// ====================================== +// Global Authentication Functions (Single Source of Truth) +// ====================================== + +// Global authentication state (single source of truth) +let globalAuthState = null; +let globalAuthManager = null; + +// Initialize global auth manager (lazy initialization) +function getGlobalAuthManager() { + if (!globalAuthManager) { + // Default to localStorage for persistence across browser sessions + globalAuthManager = new AuthManager({ isolateSession: false }); + } + return globalAuthManager; +} + +// **UNIFIED GLOBAL FUNCTION**: Set authentication state (works for all methods) +function setAuthState(authData, options = {}) { + try { + console.log('🌐 setAuthState: Setting global auth state for method:', authData.method); + console.warn('šŸ” SECURITY: Using unified plaintext storage for maximum compatibility'); + + // Store in memory + globalAuthState = authData; + + // Store in browser storage using AuthManager + const authManager = new AuthManager(options); + authManager.saveAuthState(authData); + + console.log('🌐 setAuthState: Auth state saved successfully'); + } catch (error) { + console.error('🌐 setAuthState: Failed to save auth state:', error); + throw error; + } +} + +// **UNIFIED GLOBAL FUNCTION**: Get authentication state (single source of truth) +function getAuthState() { + try { + // Always query from storage as the authoritative source + const authManager = getGlobalAuthManager(); + const storageKey = 'nostr_login_lite_auth'; + + // Check both session and local storage for compatibility + let stored = null; + if (sessionStorage.getItem(storageKey)) { + stored = sessionStorage.getItem(storageKey); + } else if (localStorage.getItem(storageKey)) { + stored = localStorage.getItem(storageKey); + } + + if (!stored) { + console.log('🌐 getAuthState: No auth state found in storage'); + globalAuthState = null; + return null; + } + + const authState = JSON.parse(stored); + console.log('🌐 getAuthState: Retrieved auth state:', authState.method); + + // Update in-memory cache + globalAuthState = authState; + return authState; + + } catch (error) { + console.error('🌐 getAuthState: Failed to get auth state:', error); + globalAuthState = null; + return null; + } +} + +// **UNIFIED GLOBAL FUNCTION**: Clear authentication state (works for all methods) +function clearAuthState() { + try { + console.log('🌐 clearAuthState: Clearing global auth state'); + + // Clear in-memory state + globalAuthState = null; + + // Clear from both storage types for thorough cleanup + const storageKey = 'nostr_login_lite_auth'; + localStorage.removeItem(storageKey); + sessionStorage.removeItem(storageKey); + sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key + + console.log('🌐 clearAuthState: Auth state cleared from all storage locations'); + } catch (error) { + console.error('🌐 clearAuthState: Failed to clear auth state:', error); + } +} + // NIP-07 compliant window.nostr provider class WindowNostr { - constructor(nostrLite, existingNostr = null) { + constructor(nostrLite, existingNostr = null, options = {}) { this.nostrLite = nostrLite; this.authState = null; this.existingNostr = existingNostr; this.authenticatedExtension = null; - this.authManager = new AuthManager({ isolateSession: nostrLite.options?.isolateSession }); + this.options = options; this._setupEventListeners(); } + // Restore authentication state on page load + async restoreAuthState() { + console.log('šŸ” WindowNostr: === restoreAuthState ==='); + + try { + // Use simplified AuthManager for consistent restore logic + const authManager = new AuthManager(this.options); + const restoredAuth = await authManager.restoreAuthState(); + + if (restoredAuth) { + console.log('šŸ” WindowNostr: āœ… Auth state restored:', restoredAuth.method); + this.authState = restoredAuth; + + // Update global state + globalAuthState = restoredAuth; + + // Dispatch restoration event + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('nlAuthRestored', { + detail: restoredAuth + })); + } + + return restoredAuth; + } else { + console.log('šŸ” WindowNostr: āŒ No auth state to restore'); + return null; + } + + } catch (error) { + console.error('šŸ” WindowNostr: Auth restoration failed:', error); + return null; + } + } + _setupEventListeners() { // Listen for authentication events to store auth state if (typeof window !== 'undefined') { window.addEventListener('nlMethodSelected', async (event) => { + console.log('šŸ” WindowNostr: nlMethodSelected event received:', event.detail); this.authState = event.detail; // If extension method, capture the specific extension the user chose if (event.detail.method === 'extension') { this.authenticatedExtension = event.detail.extension; - console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); + console.log('šŸ” WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); } - // Use global setAuthState function for unified persistence + // Use unified global setAuthState function for all methods try { - setAuthState(event.detail, { isolateSession: this.nostrLite.options?.isolateSession }); - console.log('WindowNostr: Auth state saved via global setAuthState'); + setAuthState(event.detail, this.options); + console.log('šŸ” WindowNostr: Auth state saved via unified setAuthState'); } catch (error) { - console.error('WindowNostr: Failed to save auth state via global setAuthState:', error); + console.error('šŸ” WindowNostr: Failed to save auth state:', error); } - - // EXTENSION-FIRST: Only reinstall facade for non-extension methods - // Extensions handle their own window.nostr - don't interfere! - if (event.detail.method !== 'extension' && typeof window !== 'undefined') { - console.log('WindowNostr: Re-installing facade after', this.authState?.method, 'authentication'); - window.nostr = this; - } else if (event.detail.method === 'extension') { - console.log('WindowNostr: Extension authentication - NOT reinstalling facade'); - } - - console.log('WindowNostr: Auth state updated:', this.authState?.method); }); window.addEventListener('nlLogout', () => { + console.log('šŸ” WindowNostr: nlLogout event received'); this.authState = null; this.authenticatedExtension = null; - // Clear persistent auth state - this.authManager.clearAuthState(); - console.log('WindowNostr: Auth state cleared and persistence removed'); - - // EXTENSION-FIRST: Only reinstall facade if we're not in extension mode - if (typeof window !== 'undefined' && !this.nostrLite?.hasExtension) { - console.log('WindowNostr: Re-installing facade after logout (non-extension mode)'); - window.nostr = this; - } else { - console.log('WindowNostr: Logout in extension mode - NOT reinstalling facade'); - } + // Clear from unified storage + clearAuthState(); + console.log('šŸ” WindowNostr: Auth state cleared via unified clearAuthState'); }); } } - // Restore authentication state on page load - async restoreAuthState() { - try { - console.log('šŸ” WindowNostr: === restoreAuthState START ==='); - console.log('šŸ” WindowNostr: authManager available:', !!this.authManager); - - const restoredAuth = await this.authManager.restoreAuthState(); - console.log('šŸ” WindowNostr: authManager.restoreAuthState result:', restoredAuth); - - if (restoredAuth) { - console.log('šŸ” WindowNostr: āœ… Setting authState to restored auth'); - this.authState = restoredAuth; - console.log('šŸ” WindowNostr: this.authState now:', this.authState); - - // Handle method-specific restoration - if (restoredAuth.method === 'extension') { - console.log('šŸ” WindowNostr: Extension method - setting authenticatedExtension'); - this.authenticatedExtension = restoredAuth.extension; - console.log('šŸ” WindowNostr: authenticatedExtension set to:', this.authenticatedExtension); - } - - console.log('šŸ” WindowNostr: āœ… Authentication state restored successfully!'); - console.log('šŸ” WindowNostr: Method:', restoredAuth.method); - console.log('šŸ” WindowNostr: Pubkey:', restoredAuth.pubkey); - - // Dispatch restoration event so UI can update - if (typeof window !== 'undefined') { - console.log('šŸ” WindowNostr: Dispatching nlAuthRestored event...'); - const event = new CustomEvent('nlAuthRestored', { - detail: restoredAuth - }); - console.log('šŸ” WindowNostr: Event detail:', event.detail); - window.dispatchEvent(event); - console.log('šŸ” WindowNostr: āœ… nlAuthRestored event dispatched'); - } - - console.log('šŸ” WindowNostr: === restoreAuthState END (success) ==='); - return restoredAuth; - } else { - console.log('šŸ” WindowNostr: āŒ No authentication state to restore (null from authManager)'); - console.log('šŸ” WindowNostr: === restoreAuthState END (no restore) ==='); - return null; - } - } catch (error) { - console.error('šŸ” WindowNostr: āŒ Failed to restore auth state:', error); - console.error('šŸ” WindowNostr: Error stack:', error.stack); - console.log('šŸ” WindowNostr: === restoreAuthState END (error) ==='); - return null; - } - } - async getPublicKey() { if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); @@ -3948,7 +3994,7 @@ class WindowNostr { throw new Error('Read-only mode - cannot get public key'); default: - throw new Error(`Unsupported auth method: ${this.authState.method}`); + throw new Error('Unsupported auth method: ' + this.authState.method); } } @@ -3964,14 +4010,7 @@ class WindowNostr { switch (this.authState.method) { case 'extension': // Use the captured authenticated extension, not current window.nostr - console.log('WindowNostr: signEvent - authenticatedExtension:', this.authenticatedExtension); - console.log('WindowNostr: signEvent - authState.extension:', this.authState.extension); - console.log('WindowNostr: signEvent - existingNostr:', this.existingNostr); - const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; - console.log('WindowNostr: signEvent - using extension:', ext); - console.log('WindowNostr: signEvent - extension constructor:', ext?.constructor?.name); - if (!ext) throw new Error('Extension not available'); return await ext.signEvent(event); @@ -4000,13 +4039,13 @@ class WindowNostr { } default: - throw new Error(`Unsupported auth method: ${this.authState.method}`); + throw new Error('Unsupported auth method: ' + this.authState.method); } } async getRelays() { - // Return default relays since we removed the relays configuration - return ['wss://relay.damus.io', 'wss://nos.lol']; + // Return configured relays from nostr-lite options + return this.nostrLite.options?.relays || ['wss://relay.damus.io']; } get nip04() { @@ -4049,7 +4088,7 @@ class WindowNostr { } default: - throw new Error(`Unsupported auth method: ${this.authState.method}`); + throw new Error('Unsupported auth method: ' + this.authState.method); } }, @@ -4091,7 +4130,7 @@ class WindowNostr { } default: - throw new Error(`Unsupported auth method: ${this.authState.method}`); + throw new Error('Unsupported auth method: ' + this.authState.method); } } }; @@ -4100,18 +4139,17 @@ class WindowNostr { get nip44() { return { encrypt: async (pubkey, plaintext) => { - const authState = getAuthState(); - if (!authState) { + if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } - if (authState.method === 'readonly') { + if (this.authState.method === 'readonly') { throw new Error('Read-only mode - cannot encrypt'); } - switch (authState.method) { + switch (this.authState.method) { case 'extension': { - const ext = this.authenticatedExtension || authState.extension || this.existingNostr; + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip44.encrypt(pubkey, plaintext); } @@ -4120,41 +4158,40 @@ class WindowNostr { const { nip44, nip19 } = window.NostrTools; let secretKey; - if (authState.secret.startsWith('nsec')) { - const decoded = nip19.decode(authState.secret); + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); secretKey = decoded.data; } else { - secretKey = this._hexToUint8Array(authState.secret); + secretKey = this._hexToUint8Array(this.authState.secret); } return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { - if (!authState.signer?.bunkerSigner) { + if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } - return await authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext); + return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext); } default: - throw new Error('Unsupported auth method: ' + authState.method); + throw new Error('Unsupported auth method: ' + this.authState.method); } }, decrypt: async (pubkey, ciphertext) => { - const authState = getAuthState(); - if (!authState) { + if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } - if (authState.method === 'readonly') { + if (this.authState.method === 'readonly') { throw new Error('Read-only mode - cannot decrypt'); } - switch (authState.method) { + switch (this.authState.method) { case 'extension': { - const ext = this.authenticatedExtension || authState.extension || this.existingNostr; + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip44.decrypt(pubkey, ciphertext); } @@ -4163,25 +4200,25 @@ class WindowNostr { const { nip44, nip19 } = window.NostrTools; let secretKey; - if (authState.secret.startsWith('nsec')) { - const decoded = nip19.decode(authState.secret); + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); secretKey = decoded.data; } else { - secretKey = this._hexToUint8Array(authState.secret); + secretKey = this._hexToUint8Array(this.authState.secret); } return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { - if (!authState.signer?.bunkerSigner) { + if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } - return await authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext); + return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext); } default: - throw new Error('Unsupported auth method: ' + authState.method); + throw new Error('Unsupported auth method: ' + this.authState.method); } } }; @@ -4199,171 +4236,6 @@ class WindowNostr { } } -// ====================================== -// Global Authentication State Manager - Single Source of Truth -// ====================================== - -// Storage-based authentication state - works regardless of extension presence -function getAuthState() { - try { - console.log('šŸ” getAuthState: === GLOBAL AUTH STATE CHECK ==='); - - const storageKey = 'nostr_login_lite_auth'; - let stored = null; - let storageType = null; - - // Check sessionStorage first (per-window isolation), then localStorage - if (sessionStorage.getItem(storageKey)) { - stored = sessionStorage.getItem(storageKey); - storageType = 'sessionStorage'; - console.log('šŸ” getAuthState: Found auth in sessionStorage'); - } else if (localStorage.getItem(storageKey)) { - stored = localStorage.getItem(storageKey); - storageType = 'localStorage'; - console.log('šŸ” getAuthState: Found auth in localStorage'); - } - - if (!stored) { - console.log('šŸ” getAuthState: āŒ No stored auth state found'); - return null; - } - - const authState = JSON.parse(stored); - console.log('šŸ” getAuthState: āœ… Parsed stored auth state from', storageType); - console.log('šŸ” getAuthState: Method:', authState.method); - console.log('šŸ” getAuthState: Pubkey:', authState.pubkey); - console.log('šŸ” getAuthState: Age (ms):', Date.now() - authState.timestamp); - - // Check if auth state is expired - const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000; - if (Date.now() - authState.timestamp > maxAge) { - console.log('šŸ” getAuthState: āŒ Auth state expired, clearing'); - sessionStorage.removeItem(storageKey); - localStorage.removeItem(storageKey); - return null; - } - - console.log('šŸ” getAuthState: āœ… Valid auth state found'); - return authState; - - } catch (error) { - console.error('šŸ” getAuthState: āŒ Error reading auth state:', error); - return null; - } -} - -// ====================================== -// Global Authentication State Management - Unified Persistence -// ====================================== - -// Global setAuthState function for unified persistence across all authentication methods -function setAuthState(authData, options = {}) { - try { - console.log('šŸ” setAuthState: === GLOBAL AUTH STATE SAVE ==='); - console.log('šŸ” setAuthState: authData:', authData); - console.log('šŸ” setAuthState: options:', options); - - const storageKey = 'nostr_login_lite_auth'; - - // Determine which storage to use based on isolateSession option - const storage = options.isolateSession ? sessionStorage : localStorage; - const storageType = options.isolateSession ? 'sessionStorage' : 'localStorage'; - - console.log('šŸ” setAuthState: Using', storageType, 'for persistence'); - - // Create auth state object - const authState = { - method: authData.method, - timestamp: Date.now(), - pubkey: authData.pubkey - }; - - // Add method-specific data (but no secrets for extension method) - switch (authData.method) { - case 'extension': - // For extensions, only store verification data - no secrets - authState.extensionVerification = { - constructor: authData.extension?.constructor?.name, - hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function', - hasSignEvent: typeof authData.extension?.signEvent === 'function' - }; - console.log('šŸ” setAuthState: Extension method - storing verification data only'); - break; - - case 'local': - // For local keys, store the secret (will be encrypted by AuthManager if needed) - if (authData.secret) { - authState.secret = authData.secret; - console.log('šŸ” setAuthState: Local method - storing secret key'); - } - break; - - case 'nip46': - // For NIP-46, store connection parameters - if (authData.signer) { - authState.nip46 = { - remotePubkey: authData.signer.remotePubkey, - relays: authData.signer.relays, - // Don't store secret - user will need to reconnect - }; - console.log('šŸ” setAuthState: NIP-46 method - storing connection parameters'); - } - break; - - case 'readonly': - // Read-only mode has no additional data to store - console.log('šŸ” setAuthState: Read-only method - storing basic auth state'); - break; - - default: - console.warn('šŸ” setAuthState: Unknown auth method:', authData.method); - break; - } - - // Store the auth state - storage.setItem(storageKey, JSON.stringify(authState)); - console.log('šŸ” setAuthState: āœ… Auth state saved successfully'); - console.log('šŸ” setAuthState: Final auth state:', authState); - - return authState; - - } catch (error) { - console.error('šŸ” setAuthState: āŒ Error saving auth state:', error); - throw error; - } -} - -// ====================================== -// Global Authentication State Clearing -// ====================================== - -// Global clearAuthState function for unified auth state clearing -function clearAuthState() { - try { - console.log('šŸ” clearAuthState: === GLOBAL AUTH STATE CLEAR ==='); - - const storageKey = 'nostr_login_lite_auth'; - - // Clear from both storage types to ensure complete cleanup - if (typeof sessionStorage !== 'undefined') { - sessionStorage.removeItem(storageKey); - sessionStorage.removeItem('nostr_session_key'); - console.log('šŸ” clearAuthState: āœ… Cleared auth state from sessionStorage'); - } - - if (typeof localStorage !== 'undefined') { - localStorage.removeItem(storageKey); - console.log('šŸ” clearAuthState: āœ… Cleared auth state from localStorage'); - } - - console.log('šŸ” clearAuthState: āœ… All auth state cleared successfully'); - - } catch (error) { - console.error('šŸ” clearAuthState: āŒ Error clearing auth state:', error); - } -} - - // Initialize and export if (typeof window !== 'undefined') { const nostrLite = new NostrLite(); @@ -4389,9 +4261,9 @@ if (typeof window !== 'undefined') { updateFloatingTab: (options) => nostrLite.updateFloatingTab(options), getFloatingTabState: () => nostrLite.getFloatingTabState(), - // GLOBAL AUTHENTICATION STATE API - Single Source of Truth - getAuthState: getAuthState, + // Global authentication state management (single source of truth) setAuthState: setAuthState, + getAuthState: getAuthState, clearAuthState: clearAuthState, // Expose for debugging @@ -4402,7 +4274,9 @@ if (typeof window !== 'undefined') { console.log('NOSTR_LOGIN_LITE: Library loaded and ready'); console.log('NOSTR_LOGIN_LITE: Use window.NOSTR_LOGIN_LITE.init(options) to initialize'); console.log('NOSTR_LOGIN_LITE: Detected', nostrLite.extensionBridge.getExtensionCount(), 'browser extensions'); + console.warn('šŸ” SECURITY: Unified plaintext storage enabled for maximum developer usability'); } else { // Node.js environment module.exports = { NostrLite }; } + diff --git a/lite/ui/modal.js b/lite/ui/modal.js index 56eb21b..96d0609 100644 --- a/lite/ui/modal.js +++ b/lite/ui/modal.js @@ -1131,7 +1131,6 @@ class Modal { } _setAuthMethod(method, options = {}) { - // SINGLE-EXTENSION ARCHITECTURE: Handle method switching console.log('Modal: _setAuthMethod called with:', method, options); // CRITICAL: Never install facade for extension methods - leave window.nostr as the extension @@ -1154,46 +1153,57 @@ class Modal { return; } - // For non-extension methods, we need to ensure WindowNostr facade is available - console.log('Modal: Non-extension method detected:', method); + // FOR NON-EXTENSION METHODS: Force-install facade with resilience + console.log('Modal: Non-extension method - FORCE-INSTALLING facade with resilience:', method); - // Check if we have a preserved extension but no WindowNostr facade installed - const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension; - const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr'; + // Store the current extension if any (for potential restoration later) + const currentExtension = (window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null; - console.log('Modal: Method switching check:'); - console.log(' method:', method); - console.log(' hasPreservedExtension:', hasPreservedExtension); - console.log(' hasWindowNostrFacade:', hasWindowNostrFacade); - console.log(' current window.nostr constructor:', window.nostr?.constructor?.name); + // Get NostrLite instance for facade operations + const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; + if (!nostrLiteInstance || typeof nostrLiteInstance._installFacade !== 'function') { + console.error('Modal: Cannot access NostrLite instance or _installFacade method'); + // Fallback: emit event anyway + const event = new CustomEvent('nlMethodSelected', { + detail: { method, ...options } + }); + window.dispatchEvent(event); + this.close(); + return; + } - // If we have a preserved extension but no facade, install facade for method switching - if (hasPreservedExtension && !hasWindowNostrFacade) { - console.log('Modal: Installing WindowNostr facade for method switching (non-extension authentication)'); + // IMMEDIATE FACADE INSTALLATION + console.log('Modal: Installing WindowNostr facade immediately for method:', method); + const preservedExtension = nostrLiteInstance.preservedExtension || currentExtension; + nostrLiteInstance._installFacade(preservedExtension, true); + console.log('Modal: WindowNostr facade force-installed, current window.nostr:', window.nostr?.constructor?.name); + + // DELAYED FACADE RESILIENCE - Reinstall after extension override attempts + const forceReinstallFacade = () => { + console.log('Modal: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name); - // Get the NostrLite instance and install facade with preserved extension - const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; - if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') { - const preservedExtension = nostrLiteInstance.preservedExtension; - console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name); + // If facade was overridden by extension, reinstall it + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('Modal: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method); + nostrLiteInstance._installFacade(preservedExtension, true); + console.log('Modal: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name); - nostrLiteInstance._installFacade(preservedExtension); - console.log('Modal: WindowNostr facade installed for method switching'); + // Schedule another check in case of persistent extension override + setTimeout(() => { + if (window.nostr?.constructor?.name !== 'WindowNostr') { + console.log('Modal: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method); + nostrLiteInstance._installFacade(preservedExtension, true); + } + }, 1000); } else { - console.error('Modal: Cannot access NostrLite instance or _installFacade method'); + console.log('Modal: Facade persistence verified - no override detected'); } - } + }; - // If no extension at all, ensure facade is installed for local/NIP-46/readonly methods - else if (!hasPreservedExtension && !hasWindowNostrFacade) { - console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)'); - - const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; - if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') { - nostrLiteInstance._installFacade(); - console.log('Modal: WindowNostr facade installed for non-extension methods'); - } - } + // Schedule resilience checks at multiple intervals + setTimeout(forceReinstallFacade, 100); // Quick check + setTimeout(forceReinstallFacade, 500); // Main check + setTimeout(forceReinstallFacade, 1500); // Final check // Emit auth method selection const event = new CustomEvent('nlMethodSelected', {