/** * NOSTR_LOGIN_LITE - Authentication Library * * ⚠️ WARNING: THIS FILE IS AUTO-GENERATED - DO NOT EDIT MANUALLY! * ⚠️ To make changes, edit lite/build.js and run: cd lite && node build.js * ⚠️ Any manual edits to this file will be OVERWRITTEN when build.js runs! * * 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-20T19:25:01.143Z */ // Verify dependencies are loaded if (typeof window !== 'undefined') { if (!window.NostrTools) { console.error('NOSTR_LOGIN_LITE: nostr.bundle.js must be loaded first'); throw new Error('Missing dependency: nostr.bundle.js'); } console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓'); console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools)); console.log('NOSTR_LOGIN_LITE: NIP-06 available:', !!window.NostrTools.nip06); console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46); } // ====================================== // NOSTR_LOGIN_LITE Components // ====================================== // ====================================== // CSS-Only Theme System // ====================================== const THEME_CSS = { 'default': `/** * NOSTR_LOGIN_LITE - Default Monospace Theme * Black/white/red color scheme with monospace typography * Simplified 14-variable system (6 core + 8 floating tab) */ :root { /* Core Variables (6) */ --nl-primary-color: #000000; --nl-secondary-color: #ffffff; --nl-accent-color: #ff0000; --nl-muted-color: #CCCCCC; --nl-font-family: "Courier New", Courier, monospace; --nl-border-radius: 15px; --nl-border-width: 3px; /* Floating Tab Variables (8) */ --nl-tab-bg-logged-out: #ffffff; --nl-tab-bg-logged-in: #ffffff; --nl-tab-bg-opacity-logged-out: 0.9; --nl-tab-bg-opacity-logged-in: 0.2; --nl-tab-color-logged-out: #000000; --nl-tab-color-logged-in: #ffffff; --nl-tab-border-logged-out: #000000; --nl-tab-border-logged-in: #ff0000; --nl-tab-border-opacity-logged-out: 1.0; --nl-tab-border-opacity-logged-in: 0.1; } /* Base component styles using simplified variables */ .nl-component { font-family: var(--nl-font-family); color: var(--nl-primary-color); } .nl-button { background: var(--nl-secondary-color); color: var(--nl-primary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); font-family: var(--nl-font-family); cursor: pointer; transition: all 0.2s ease; } .nl-button:hover { border-color: var(--nl-accent-color); } .nl-button:active { background: var(--nl-accent-color); color: var(--nl-secondary-color); } .nl-input { background: var(--nl-secondary-color); color: var(--nl-primary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); font-family: var(--nl-font-family); box-sizing: border-box; } .nl-input:focus { border-color: var(--nl-accent-color); outline: none; } .nl-container { background: var(--nl-secondary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); } .nl-title, .nl-heading { font-family: var(--nl-font-family); color: var(--nl-primary-color); margin: 0; } .nl-text { font-family: var(--nl-font-family); color: var(--nl-primary-color); } .nl-text--muted { color: var(--nl-muted-color); } .nl-icon { font-family: var(--nl-font-family); color: var(--nl-primary-color); } /* Floating tab styles */ .nl-floating-tab { font-family: var(--nl-font-family); border-radius: var(--nl-border-radius); border: var(--nl-border-width) solid; transition: all 0.2s ease; } .nl-floating-tab--logged-out { background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out)); color: var(--nl-tab-color-logged-out); border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out)); } .nl-floating-tab--logged-in { background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in)); color: var(--nl-tab-color-logged-in); border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in)); } .nl-transition { transition: all 0.2s ease; }`, 'dark': `/** * NOSTR_LOGIN_LITE - Dark Monospace Theme */ :root { /* Core Variables (6) */ --nl-primary-color: #white; --nl-secondary-color: #black; --nl-accent-color: #ff0000; --nl-muted-color: #666666; --nl-font-family: "Courier New", Courier, monospace; --nl-border-radius: 15px; --nl-border-width: 3px; /* Floating Tab Variables (8) */ --nl-tab-bg-logged-out: #ffffff; --nl-tab-bg-logged-in: #000000; --nl-tab-bg-opacity-logged-out: 0.9; --nl-tab-bg-opacity-logged-in: 0.8; --nl-tab-color-logged-out: #000000; --nl-tab-color-logged-in: #ffffff; --nl-tab-border-logged-out: #000000; --nl-tab-border-logged-in: #ff0000; --nl-tab-border-opacity-logged-out: 1.0; --nl-tab-border-opacity-logged-in: 0.9; } /* Base component styles using simplified variables */ .nl-component { font-family: var(--nl-font-family); color: var(--nl-primary-color); } .nl-button { background: var(--nl-secondary-color); color: var(--nl-primary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); font-family: var(--nl-font-family); cursor: pointer; transition: all 0.2s ease; } .nl-button:hover { border-color: var(--nl-accent-color); } .nl-button:active { background: var(--nl-accent-color); color: var(--nl-secondary-color); } .nl-input { background: var(--nl-secondary-color); color: var(--nl-primary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); font-family: var(--nl-font-family); box-sizing: border-box; } .nl-input:focus { border-color: var(--nl-accent-color); outline: none; } .nl-container { background: var(--nl-secondary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); } .nl-title, .nl-heading { font-family: var(--nl-font-family); color: var(--nl-primary-color); margin: 0; } .nl-text { font-family: var(--nl-font-family); color: var(--nl-primary-color); } .nl-text--muted { color: var(--nl-muted-color); } .nl-icon { font-family: var(--nl-font-family); color: var(--nl-primary-color); } /* Floating tab styles */ .nl-floating-tab { font-family: var(--nl-font-family); border-radius: var(--nl-border-radius); border: var(--nl-border-width) solid; transition: all 0.2s ease; } .nl-floating-tab--logged-out { background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out)); color: var(--nl-tab-color-logged-out); border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out)); } .nl-floating-tab--logged-in { background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in)); color: var(--nl-tab-color-logged-in); border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in)); } .nl-transition { transition: all 0.2s ease; }` }; // Theme management functions function injectThemeCSS(themeName = 'default') { if (typeof document !== 'undefined') { // Remove existing theme CSS const existingStyle = document.getElementById('nl-theme-css'); if (existingStyle) { existingStyle.remove(); } // Inject selected theme CSS const themeCss = THEME_CSS[themeName] || THEME_CSS['default']; const style = document.createElement('style'); style.id = 'nl-theme-css'; style.textContent = themeCss; document.head.appendChild(style); console.log(`NOSTR_LOGIN_LITE: ${themeName} theme CSS injected`); } } // Auto-inject default theme when DOM is ready if (typeof document !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => injectThemeCSS('default')); } else { injectThemeCSS('default'); } } // ====================================== // Modal UI Component // ====================================== class Modal { constructor(options = {}) { this.options = options; this.container = null; this.isVisible = false; this.currentScreen = null; this.isEmbedded = !!options.embedded; this.embeddedContainer = options.embedded; // Initialize modal container and styles this._initModal(); } _initModal() { // Create modal container this.container = document.createElement('div'); this.container.id = this.isEmbedded ? 'nl-modal-embedded' : 'nl-modal'; if (this.isEmbedded) { // Embedded mode: inline positioning, no overlay this.container.style.cssText = ` position: relative; display: none; font-family: var(--nl-font-family, 'Courier New', monospace); width: 100%; `; } else { // Modal mode: fixed overlay this.container.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.75); display: none; z-index: 10000; font-family: var(--nl-font-family, 'Courier New', monospace); `; } // Create modal content const modalContent = document.createElement('div'); if (this.isEmbedded) { // Embedded content: no centering margin, full width modalContent.style.cssText = ` position: relative; background: var(--nl-secondary-color); color: var(--nl-primary-color); width: 100%; border-radius: var(--nl-border-radius, 15px); border: var(--nl-border-width) solid var(--nl-primary-color); overflow: hidden; `; } else { // Modal content: centered with margin, no fixed height modalContent.style.cssText = ` position: relative; background: var(--nl-secondary-color); color: var(--nl-primary-color); width: 90%; max-width: 400px; margin: 50px auto; border-radius: var(--nl-border-radius, 15px); border: var(--nl-border-width) solid var(--nl-primary-color); overflow: hidden; `; } // Header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` padding: 20px 24px 0 24px; display: flex; justify-content: space-between; align-items: center; background: transparent; border-bottom: none; `; const modalTitle = document.createElement('h2'); modalTitle.textContent = 'Nostr Login'; modalTitle.style.cssText = ` margin: 0; font-size: 24px; font-weight: 600; color: var(--nl-primary-color); font-family: var(--nl-font-family, 'Courier New', monospace); `; modalHeader.appendChild(modalTitle); // Only add close button for non-embedded modals // Embedded modals shouldn't have a close button because there's no way to reopen them if (!this.isEmbedded) { const closeButton = document.createElement('button'); closeButton.innerHTML = '×'; closeButton.onclick = () => this.close(); closeButton.style.cssText = ` background: var(--nl-secondary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: 4px; font-size: 28px; color: var(--nl-primary-color); cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-family: var(--nl-font-family, 'Courier New', monospace); `; closeButton.onmouseover = () => { closeButton.style.borderColor = 'var(--nl-accent-color)'; closeButton.style.background = 'var(--nl-secondary-color)'; }; closeButton.onmouseout = () => { closeButton.style.borderColor = 'var(--nl-primary-color)'; closeButton.style.background = 'var(--nl-secondary-color)'; }; modalHeader.appendChild(closeButton); } // Body this.modalBody = document.createElement('div'); this.modalBody.style.cssText = ` padding: 24px; background: transparent; font-family: var(--nl-font-family, 'Courier New', monospace); `; modalContent.appendChild(modalHeader); modalContent.appendChild(this.modalBody); this.container.appendChild(modalContent); // Add to appropriate parent if (this.isEmbedded && this.embeddedContainer) { // Append to specified container for embedding if (typeof this.embeddedContainer === 'string') { const targetElement = document.querySelector(this.embeddedContainer); if (targetElement) { targetElement.appendChild(this.container); } else { console.error('NOSTR_LOGIN_LITE: Embedded container not found:', this.embeddedContainer); document.body.appendChild(this.container); } } else if (this.embeddedContainer instanceof HTMLElement) { this.embeddedContainer.appendChild(this.container); } else { console.error('NOSTR_LOGIN_LITE: Invalid embedded container'); document.body.appendChild(this.container); } } else { // Add to body for modal mode document.body.appendChild(this.container); } // Click outside to close (only for modal mode) if (!this.isEmbedded) { this.container.onclick = (e) => { if (e.target === this.container) { this.close(); } }; } // Update theme this.updateTheme(); } updateTheme() { // The theme will automatically update through CSS custom properties // No manual styling needed - the CSS variables handle everything } open(opts = {}) { this.currentScreen = opts.startScreen; this.isVisible = true; this.container.style.display = 'block'; // Render login options this._renderLoginOptions(); } close() { this.isVisible = false; this.container.style.display = 'none'; this.modalBody.innerHTML = ''; } _renderLoginOptions() { this.modalBody.innerHTML = ''; const options = []; // Extension option if (this.options?.methods?.extension !== false) { options.push({ type: 'extension', title: 'Browser Extension', description: 'Use your browser extension', icon: '🔌' }); } // Local key option if (this.options?.methods?.local !== false) { options.push({ type: 'local', title: 'Local Key', description: 'Create or import your own key', icon: '🔑' }); } // Seed Phrase option - only show if explicitly enabled if (this.options?.methods?.seedphrase === true) { options.push({ type: 'seedphrase', title: 'Seed Phrase', description: 'Import from mnemonic seed phrase', icon: '🌱' }); } // Nostr Connect option (check both 'connect' and 'remote' for compatibility) if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) { options.push({ type: 'connect', title: 'Nostr Connect', description: 'Connect with external signer', icon: '🌐' }); } // Read-only option if (this.options?.methods?.readonly !== false) { options.push({ type: 'readonly', title: 'Read Only', description: 'Browse without signing', icon: '👁️' }); } // OTP/DM option if (this.options?.methods?.otp !== false) { options.push({ type: 'otp', title: 'DM/OTP', description: 'Receive OTP via DM', icon: '📱' }); } // Render each option options.forEach(option => { const button = document.createElement('button'); button.onclick = () => this._handleOptionClick(option.type); button.style.cssText = ` display: flex; align-items: center; width: 100%; padding: 16px; margin-bottom: 12px; background: var(--nl-secondary-color); color: var(--nl-primary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); cursor: pointer; transition: all 0.2s; font-family: var(--nl-font-family, 'Courier New', monospace); `; button.onmouseover = () => { button.style.borderColor = 'var(--nl-accent-color)'; button.style.background = 'var(--nl-secondary-color)'; }; button.onmouseout = () => { button.style.borderColor = 'var(--nl-primary-color)'; button.style.background = 'var(--nl-secondary-color)'; }; const iconDiv = document.createElement('div'); // Remove the icon entirely - no emojis or text-based icons iconDiv.textContent = ''; iconDiv.style.cssText = ` font-size: 16px; font-weight: bold; margin-right: 16px; width: 0px; text-align: center; color: var(--nl-primary-color); font-family: var(--nl-font-family, 'Courier New', monospace); `; const contentDiv = document.createElement('div'); contentDiv.style.cssText = 'flex: 1; text-align: left;'; const titleDiv = document.createElement('div'); titleDiv.textContent = option.title; titleDiv.style.cssText = ` font-weight: 600; margin-bottom: 4px; color: var(--nl-primary-color); font-family: var(--nl-font-family, 'Courier New', monospace); `; const descDiv = document.createElement('div'); descDiv.textContent = option.description; descDiv.style.cssText = ` font-size: 14px; color: #666666; font-family: var(--nl-font-family, 'Courier New', monospace); `; contentDiv.appendChild(titleDiv); contentDiv.appendChild(descDiv); button.appendChild(iconDiv); button.appendChild(contentDiv); this.modalBody.appendChild(button); }); } _handleOptionClick(type) { console.log('Selected login type:', type); // Handle different login types switch (type) { case 'extension': this._handleExtension(); break; case 'local': this._showLocalKeyScreen(); break; case 'seedphrase': this._showSeedPhraseScreen(); break; case 'connect': this._showConnectScreen(); break; case 'readonly': this._handleReadonly(); break; case 'otp': this._showOtpScreen(); break; } } _handleExtension() { // SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension let extension = null; // Check if NostrLite instance has a preserved extension (real extension detected at init) if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) { extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension; console.log('Modal: Using preserved extension:', extension.constructor?.name); } // Otherwise check current window.nostr else if (window.nostr && this._isRealExtension(window.nostr)) { extension = window.nostr; console.log('Modal: Using current window.nostr extension:', extension.constructor?.name); } if (!extension) { console.log('Modal: No extension detected yet, waiting for deferred detection...'); // DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library let attempts = 0; const maxAttempts = 10; // Try for 2 seconds const checkForExtension = () => { attempts++; // Check again for preserved extension (might be set by deferred detection) if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) { extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension; console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name); this._tryExtensionLogin(extension); return; } // Check current window.nostr again if (window.nostr && this._isRealExtension(window.nostr)) { extension = window.nostr; console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name); this._tryExtensionLogin(extension); return; } // Keep trying or give up if (attempts < maxAttempts) { setTimeout(checkForExtension, 200); } else { console.log('Modal: No browser extension found after waiting 2 seconds'); this._showExtensionRequired(); } }; // Start checking after a brief delay setTimeout(checkForExtension, 200); return; } // Use the single detected extension directly - no choice UI console.log('Modal: Single extension mode - using extension directly'); this._tryExtensionLogin(extension); } _detectAllExtensions() { const extensions = []; const seenExtensions = new Set(); // Track extensions by object reference to avoid duplicates // Extension locations to check (in priority order) const locations = [ { path: 'window.navigator?.nostr', name: 'navigator.nostr', displayName: 'Standard Extension (navigator.nostr)', icon: '🌐', getter: () => window.navigator?.nostr }, { path: 'window.webln?.nostr', name: 'webln.nostr', displayName: 'Alby WebLN Extension', icon: '⚡', getter: () => window.webln?.nostr }, { path: 'window.alby?.nostr', name: 'alby.nostr', displayName: 'Alby Extension (Direct)', icon: '🐝', getter: () => window.alby?.nostr }, { path: 'window.nos2x', name: 'nos2x', displayName: 'nos2x Extension', icon: '🔌', getter: () => window.nos2x }, { path: 'window.flamingo?.nostr', name: 'flamingo.nostr', displayName: 'Flamingo Extension', icon: '🦩', getter: () => window.flamingo?.nostr }, { path: 'window.mutiny?.nostr', name: 'mutiny.nostr', displayName: 'Mutiny Extension', icon: '⚔️', getter: () => window.mutiny?.nostr }, { path: 'window.nostrich?.nostr', name: 'nostrich.nostr', displayName: 'Nostrich Extension', icon: '🐦', getter: () => window.nostrich?.nostr }, { path: 'window.getAlby?.nostr', name: 'getAlby.nostr', displayName: 'getAlby Extension', icon: '🔧', getter: () => window.getAlby?.nostr } ]; // Check each location for (const location of locations) { try { const obj = location.getter(); console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name); if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) { extensions.push({ name: location.name, displayName: location.displayName, icon: location.icon, extension: obj }); seenExtensions.add(obj); console.log(`Modal: ✓ Detected extension at ${location.name} (${obj.constructor?.name})`); } else if (obj) { console.log(`Modal: ✗ Filtered out ${location.name} (${obj.constructor?.name})`); } } catch (e) { // Location doesn't exist or can't be accessed console.log(`Modal: ${location.name} not accessible:`, e.message); } } // Also check window.nostr but be extra careful to avoid our library console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name); if (window.nostr) { // Check if window.nostr is our WindowNostr facade with a preserved extension if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) { console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension'); const preservedExtension = window.nostr.existingNostr; console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name); if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) { extensions.push({ name: 'window.nostr.existingNostr', displayName: 'Extension (preserved by WindowNostr)', icon: '🔑', extension: preservedExtension }); seenExtensions.add(preservedExtension); console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`); } } // Check if window.nostr is directly a real extension (not our facade) else if (this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) { extensions.push({ name: 'window.nostr', displayName: 'Extension (window.nostr)', icon: '🔑', extension: window.nostr }); seenExtensions.add(window.nostr); console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`); } else { console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`); } } return extensions; } _isRealExtension(obj) { console.log(`Modal: EXTENSIVE DEBUG - _isRealExtension called with:`, obj); console.log(`Modal: Object type: ${typeof obj}`); console.log(`Modal: Object truthy: ${!!obj}`); if (!obj || typeof obj !== 'object') { console.log(`Modal: REJECT - Not an object`); return false; } console.log(`Modal: getPublicKey type: ${typeof obj.getPublicKey}`); console.log(`Modal: signEvent type: ${typeof obj.signEvent}`); // Must have required Nostr methods if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { console.log(`Modal: REJECT - Missing required methods`); return false; } // Exclude NostrTools library object if (obj === window.NostrTools) { console.log(`Modal: REJECT - Is NostrTools object`); return false; } // Use the EXACT SAME logic as the comprehensive test (lines 804-809) // This is the key fix - match the comprehensive test's successful detection logic const constructorName = obj.constructor?.name; const objectKeys = Object.keys(obj); console.log(`Modal: Constructor name: "${constructorName}"`); console.log(`Modal: Object keys: [${objectKeys.join(', ')}]`); // COMPREHENSIVE TEST LOGIC - Accept anything with required methods that's not our specific library classes const isRealExtension = ( typeof obj.getPublicKey === 'function' && typeof obj.signEvent === 'function' && constructorName !== 'WindowNostr' && // Our library class constructorName !== 'NostrLite' // Our main class ); console.log(`Modal: Using comprehensive test logic:`); console.log(` Has getPublicKey: ${typeof obj.getPublicKey === 'function'}`); console.log(` Has signEvent: ${typeof obj.signEvent === 'function'}`); console.log(` Not WindowNostr: ${constructorName !== 'WindowNostr'}`); console.log(` Not NostrLite: ${constructorName !== 'NostrLite'}`); console.log(` Constructor: "${constructorName}"`); // Additional debugging for comparison const extensionPropChecks = { _isEnabled: !!obj._isEnabled, enabled: !!obj.enabled, kind: !!obj.kind, _eventEmitter: !!obj._eventEmitter, _scope: !!obj._scope, _requests: !!obj._requests, _pubkey: !!obj._pubkey, name: !!obj.name, version: !!obj.version, description: !!obj.description }; console.log(`Modal: Extension property analysis:`, extensionPropChecks); const hasExtensionProps = !!( obj._isEnabled || obj.enabled || obj.kind || obj._eventEmitter || obj._scope || obj._requests || obj._pubkey || obj.name || obj.version || obj.description ); const underscoreKeys = objectKeys.filter(key => key.startsWith('_')); const hexToUint8Keys = objectKeys.filter(key => key.startsWith('_hex')); console.log(`Modal: Underscore keys: [${underscoreKeys.join(', ')}]`); console.log(`Modal: _hex* keys: [${hexToUint8Keys.join(', ')}]`); console.log(`Modal: Additional analysis:`); console.log(` hasExtensionProps: ${hasExtensionProps}`); console.log(` hasLibraryMethod (_hexToUint8Array): ${objectKeys.includes('_hexToUint8Array')}`); console.log(`Modal: COMPREHENSIVE TEST LOGIC RESULT: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); console.log(`Modal: FINAL DECISION for ${constructorName}: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); return isRealExtension; } _showExtensionChoice(extensions) { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Choose Browser Extension'; title.style.cssText = ` margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: var(--nl-primary-color); font-family: var(--nl-font-family, 'Courier New', monospace); `; const description = document.createElement('p'); description.textContent = `Found ${extensions.length} Nostr extensions. Choose which one to use:`; description.style.cssText = ` margin-bottom: 20px; color: #666666; font-size: 14px; font-family: var(--nl-font-family, 'Courier New', monospace); `; this.modalBody.appendChild(title); this.modalBody.appendChild(description); // Create button for each extension extensions.forEach((ext, index) => { const button = document.createElement('button'); button.onclick = () => this._tryExtensionLogin(ext.extension); button.style.cssText = ` display: flex; align-items: center; width: 100%; padding: 16px; margin-bottom: 12px; background: var(--nl-secondary-color); color: var(--nl-primary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); cursor: pointer; transition: all 0.2s; text-align: left; font-family: var(--nl-font-family, 'Courier New', monospace); `; button.onmouseover = () => { button.style.borderColor = 'var(--nl-accent-color)'; button.style.background = 'var(--nl-secondary-color)'; }; button.onmouseout = () => { button.style.borderColor = 'var(--nl-primary-color)'; button.style.background = 'var(--nl-secondary-color)'; }; const iconDiv = document.createElement('div'); iconDiv.textContent = ext.icon; iconDiv.style.cssText = ` font-size: 24px; margin-right: 16px; width: 24px; text-align: center; `; const contentDiv = document.createElement('div'); contentDiv.style.cssText = 'flex: 1;'; const nameDiv = document.createElement('div'); nameDiv.textContent = ext.displayName; nameDiv.style.cssText = ` font-weight: 600; margin-bottom: 4px; color: var(--nl-primary-color); font-family: var(--nl-font-family, 'Courier New', monospace); `; const pathDiv = document.createElement('div'); pathDiv.textContent = ext.name; pathDiv.style.cssText = ` font-size: 12px; color: #666666; font-family: var(--nl-font-family, 'Courier New', monospace); `; contentDiv.appendChild(nameDiv); contentDiv.appendChild(pathDiv); button.appendChild(iconDiv); button.appendChild(contentDiv); this.modalBody.appendChild(button); }); // Add back button const backButton = document.createElement('button'); backButton.textContent = 'Back to Login Options'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 20px;'; this.modalBody.appendChild(backButton); } async _tryExtensionLogin(extensionObj) { try { // Show loading state this.modalBody.innerHTML = '
🔄 Connecting to extension...
'; // Get pubkey from extension const pubkey = await extensionObj.getPublicKey(); console.log('Extension provided pubkey:', pubkey); // Set extension method with the extension object this._setAuthMethod('extension', { pubkey, extension: extensionObj }); } catch (error) { console.error('Extension login failed:', error); this._showError(`Extension login failed: ${error.message}`); } } _showLocalKeyScreen() { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Local Key'; title.style.cssText = 'margin: 0 0 20px 0; font-size: 18px; font-weight: 600;'; const createButton = document.createElement('button'); createButton.textContent = 'Create New Key'; createButton.onclick = () => this._createLocalKey(); createButton.style.cssText = this._getButtonStyle(); const importButton = document.createElement('button'); importButton.textContent = 'Import Existing Key'; importButton.onclick = () => this._showImportKeyForm(); importButton.style.cssText = this._getButtonStyle() + 'margin-top: 12px;'; const backButton = document.createElement('button'); backButton.textContent = 'Back'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = ` display: block; margin-top: 20px; padding: 12px; background: #6b7280; color: white; border: none; border-radius: 6px; cursor: pointer; `; this.modalBody.appendChild(title); this.modalBody.appendChild(createButton); this.modalBody.appendChild(importButton); this.modalBody.appendChild(backButton); } _createLocalKey() { try { const sk = window.NostrTools.generateSecretKey(); const pk = window.NostrTools.getPublicKey(sk); const nsec = window.NostrTools.nip19.nsecEncode(sk); const npub = window.NostrTools.nip19.npubEncode(pk); this._showKeyDisplay(pk, nsec, 'created'); } catch (error) { this._showError('Failed to create key: ' + error.message); } } _showImportKeyForm() { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Import Local Key'; title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;'; const description = document.createElement('p'); description.textContent = 'Enter your secret key in either nsec or hex format:'; description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;'; const textarea = document.createElement('textarea'); textarea.placeholder = 'Enter your secret key:\n• nsec1... (bech32 format)\n• 64-character hex string'; textarea.style.cssText = ` width: 100%; height: 100px; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 12px; resize: none; font-family: monospace; font-size: 14px; box-sizing: border-box; `; // Add real-time format detection const formatHint = document.createElement('div'); formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;'; textarea.oninput = () => { const value = textarea.value.trim(); if (!value) { formatHint.textContent = ''; return; } const format = this._detectKeyFormat(value); if (format === 'nsec') { formatHint.textContent = '✅ Valid nsec format detected'; formatHint.style.color = '#059669'; } else if (format === 'hex') { formatHint.textContent = '✅ Valid hex format detected'; formatHint.style.color = '#059669'; } else { formatHint.textContent = '❌ Invalid key format - must be nsec1... or 64-character hex'; formatHint.style.color = '#dc2626'; } }; const importButton = document.createElement('button'); importButton.textContent = 'Import Key'; importButton.onclick = () => this._importLocalKey(textarea.value); importButton.style.cssText = this._getButtonStyle(); const backButton = document.createElement('button'); backButton.textContent = 'Back'; backButton.onclick = () => this._showLocalKeyScreen(); backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; this.modalBody.appendChild(title); this.modalBody.appendChild(description); this.modalBody.appendChild(textarea); this.modalBody.appendChild(formatHint); this.modalBody.appendChild(importButton); this.modalBody.appendChild(backButton); } _detectKeyFormat(keyValue) { const trimmed = keyValue.trim(); // Check for nsec format if (trimmed.startsWith('nsec1') && trimmed.length === 63) { try { window.NostrTools.nip19.decode(trimmed); return 'nsec'; } catch { return 'invalid'; } } // Check for hex format (64 characters, valid hex) if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) { return 'hex'; } return 'invalid'; } _importLocalKey(keyValue) { try { const trimmed = keyValue.trim(); if (!trimmed) { throw new Error('Please enter a secret key'); } const format = this._detectKeyFormat(trimmed); let sk; if (format === 'nsec') { // Decode nsec format - this returns Uint8Array const decoded = window.NostrTools.nip19.decode(trimmed); if (decoded.type !== 'nsec') { throw new Error('Invalid nsec format'); } sk = decoded.data; // This is already Uint8Array } else if (format === 'hex') { // Convert hex string to Uint8Array sk = this._hexToUint8Array(trimmed); // Test that it's a valid secret key by trying to get public key window.NostrTools.getPublicKey(sk); } else { throw new Error('Invalid key format. Please enter either nsec1... or 64-character hex string'); } // Generate public key and encoded formats const pk = window.NostrTools.getPublicKey(sk); const nsec = window.NostrTools.nip19.nsecEncode(sk); const npub = window.NostrTools.nip19.npubEncode(pk); this._showKeyDisplay(pk, nsec, 'imported'); } catch (error) { this._showError('Invalid key: ' + error.message); } } _hexToUint8Array(hex) { // Convert hex string to Uint8Array if (hex.length % 2 !== 0) { throw new Error('Invalid hex string length'); } const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(hex.substr(i * 2, 2), 16); } return bytes; } _showKeyDisplay(pubkey, nsec, action) { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); title.textContent = `Key ${action} successfully!`; title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;'; const warningDiv = document.createElement('div'); warningDiv.textContent = '⚠️ Save your secret key securely!'; warningDiv.style.cssText = 'background: #fef3c7; color: #92400e; padding: 12px; border-radius: 6px; margin-bottom: 16px; font-size: 12px;'; // Helper function to create copy button const createCopyButton = (text, label) => { const copyBtn = document.createElement('button'); copyBtn.textContent = `Copy ${label}`; copyBtn.style.cssText = ` margin-left: 8px; padding: 4px 8px; font-size: 10px; background: var(--nl-secondary-color); color: var(--nl-primary-color); border: 1px solid var(--nl-primary-color); border-radius: 4px; cursor: pointer; font-family: var(--nl-font-family, 'Courier New', monospace); `; copyBtn.onclick = async (e) => { e.preventDefault(); try { await navigator.clipboard.writeText(text); const originalText = copyBtn.textContent; copyBtn.textContent = '✓ Copied!'; copyBtn.style.color = '#059669'; setTimeout(() => { copyBtn.textContent = originalText; copyBtn.style.color = 'var(--nl-primary-color)'; }, 2000); } catch (err) { console.error('Failed to copy:', err); copyBtn.textContent = '✗ Failed'; copyBtn.style.color = '#dc2626'; setTimeout(() => { copyBtn.textContent = originalText; copyBtn.style.color = 'var(--nl-primary-color)'; }, 2000); } }; return copyBtn; }; // Convert pubkey to hex for verification const pubkeyHex = typeof pubkey === 'string' ? pubkey : Array.from(pubkey).map(b => b.toString(16).padStart(2, '0')).join(''); // Decode nsec to get secret key as hex let secretKeyHex = ''; try { const decoded = window.NostrTools.nip19.decode(nsec); secretKeyHex = Array.from(decoded.data).map(b => b.toString(16).padStart(2, '0')).join(''); } catch (err) { console.error('Failed to decode nsec for hex display:', err); } // Secret Key Section const nsecSection = document.createElement('div'); nsecSection.style.cssText = 'margin-bottom: 16px;'; const nsecLabel = document.createElement('div'); nsecLabel.innerHTML = 'Your Secret Key (nsec):'; nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; const nsecContainer = document.createElement('div'); nsecContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;'; const nsecCode = document.createElement('code'); nsecCode.textContent = nsec; nsecCode.style.cssText = ` flex: 1; word-break: break-all; background: #f3f4f6; padding: 6px; border-radius: 4px; font-size: 10px; line-height: 1.3; font-family: 'Courier New', monospace; display: block; `; nsecContainer.appendChild(nsecCode); nsecContainer.appendChild(createCopyButton(nsec, 'nsec')); nsecSection.appendChild(nsecLabel); nsecSection.appendChild(nsecContainer); // Secret Key Hex Section if (secretKeyHex) { const secretHexLabel = document.createElement('div'); secretHexLabel.innerHTML = 'Secret Key (hex):'; secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; const secretHexContainer = document.createElement('div'); secretHexContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;'; const secretHexCode = document.createElement('code'); secretHexCode.textContent = secretKeyHex; secretHexCode.style.cssText = ` flex: 1; word-break: break-all; background: #f3f4f6; padding: 6px; border-radius: 4px; font-size: 10px; line-height: 1.3; font-family: 'Courier New', monospace; display: block; `; secretHexContainer.appendChild(secretHexCode); secretHexContainer.appendChild(createCopyButton(secretKeyHex, 'hex')); nsecSection.appendChild(secretHexLabel); nsecSection.appendChild(secretHexContainer); } // Public Key Section const npubSection = document.createElement('div'); npubSection.style.cssText = 'margin-bottom: 16px;'; const npub = window.NostrTools.nip19.npubEncode(pubkey); const npubLabel = document.createElement('div'); npubLabel.innerHTML = 'Your Public Key (npub):'; npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; const npubContainer = document.createElement('div'); npubContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;'; const npubCode = document.createElement('code'); npubCode.textContent = npub; npubCode.style.cssText = ` flex: 1; word-break: break-all; background: #f3f4f6; padding: 6px; border-radius: 4px; font-size: 10px; line-height: 1.3; font-family: 'Courier New', monospace; display: block; `; npubContainer.appendChild(npubCode); npubContainer.appendChild(createCopyButton(npub, 'npub')); npubSection.appendChild(npubLabel); npubSection.appendChild(npubContainer); // Public Key Hex Section const pubHexLabel = document.createElement('div'); pubHexLabel.innerHTML = 'Public Key (hex):'; pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;'; const pubHexContainer = document.createElement('div'); pubHexContainer.style.cssText = 'display: flex; align-items: flex-start;'; const pubHexCode = document.createElement('code'); pubHexCode.textContent = pubkeyHex; pubHexCode.style.cssText = ` flex: 1; word-break: break-all; background: #f3f4f6; padding: 6px; border-radius: 4px; font-size: 10px; line-height: 1.3; font-family: 'Courier New', monospace; display: block; `; pubHexContainer.appendChild(pubHexCode); pubHexContainer.appendChild(createCopyButton(pubkeyHex, 'hex')); npubSection.appendChild(pubHexLabel); npubSection.appendChild(pubHexContainer); const continueButton = document.createElement('button'); continueButton.textContent = 'Continue'; continueButton.onclick = () => this._setAuthMethod('local', { secret: nsec, pubkey }); continueButton.style.cssText = this._getButtonStyle(); this.modalBody.appendChild(title); this.modalBody.appendChild(warningDiv); this.modalBody.appendChild(nsecSection); this.modalBody.appendChild(npubSection); this.modalBody.appendChild(continueButton); } _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 if (method === 'extension') { console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension'); // Save extension authentication state using global setAuthState function if (typeof window.setAuthState === 'function') { console.log('Modal: Saving extension auth state to storage'); window.setAuthState({ method, ...options }, { isolateSession: this.options?.isolateSession }); } // Emit auth method selection directly for extension const event = new CustomEvent('nlMethodSelected', { detail: { method, ...options } }); window.dispatchEvent(event); this.close(); return; } // For non-extension methods, we need to ensure WindowNostr facade is available console.log('Modal: Non-extension method detected:', 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'; 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); // 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)'); // 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); nostrLiteInstance._installFacade(preservedExtension); console.log('Modal: WindowNostr facade installed for method switching'); } else { console.error('Modal: Cannot access NostrLite instance or _installFacade method'); } } // 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'); } } // Emit auth method selection const event = new CustomEvent('nlMethodSelected', { detail: { method, ...options } }); window.dispatchEvent(event); this.close(); } _showError(message) { this.modalBody.innerHTML = ''; const errorDiv = document.createElement('div'); errorDiv.style.cssText = 'background: #fee2e2; color: #dc2626; padding: 16px; border-radius: 6px; margin-bottom: 16px;'; errorDiv.innerHTML = `Error: ${message}`; const backButton = document.createElement('button'); backButton.textContent = 'Back'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = this._getButtonStyle('secondary'); this.modalBody.appendChild(errorDiv); this.modalBody.appendChild(backButton); } _showExtensionRequired() { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Browser Extension Required'; title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;'; const message = document.createElement('p'); message.innerHTML = ` Please install a Nostr browser extension and refresh the page.

Important: If you have multiple extensions installed, please disable all but one to avoid conflicts.

Popular extensions: Alby, nos2x, Flamingo `; message.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px; line-height: 1.4;'; const backButton = document.createElement('button'); backButton.textContent = 'Back'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = this._getButtonStyle('secondary'); this.modalBody.appendChild(title); this.modalBody.appendChild(message); this.modalBody.appendChild(backButton); } _showConnectScreen() { this.modalBody.innerHTML = ''; const description = document.createElement('p'); description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.'; description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;'; const formGroup = document.createElement('div'); formGroup.style.cssText = 'margin-bottom: 20px;'; const label = document.createElement('label'); label.textContent = 'Bunker Public Key:'; label.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;'; const pubkeyInput = document.createElement('input'); pubkeyInput.type = 'text'; pubkeyInput.placeholder = 'bunker://pubkey?relay=..., bunker:hex, hex, or npub...'; pubkeyInput.style.cssText = ` width: 100%; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 12px; font-family: monospace; box-sizing: border-box; `; // Add real-time bunker key validation const formatHint = document.createElement('div'); formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;'; const connectButton = document.createElement('button'); connectButton.textContent = 'Connect to Bunker'; connectButton.disabled = true; connectButton.onclick = () => { if (!connectButton.disabled) { this._handleNip46Connect(pubkeyInput.value); } }; // Set initial disabled state connectButton.style.cssText = ` display: block; width: 100%; padding: 12px; border: var(--nl-border-width) solid var(--nl-muted-color); border-radius: var(--nl-border-radius); font-size: 16px; font-weight: 500; cursor: not-allowed; transition: all 0.2s; font-family: var(--nl-font-family, 'Courier New', monospace); background: var(--nl-secondary-color); color: var(--nl-muted-color); margin-bottom: 12px; `; pubkeyInput.oninput = () => { const value = pubkeyInput.value.trim(); if (!value) { formatHint.textContent = ''; // Disable button connectButton.disabled = true; connectButton.style.borderColor = 'var(--nl-muted-color)'; connectButton.style.color = 'var(--nl-muted-color)'; connectButton.style.cursor = 'not-allowed'; return; } const isValid = this._validateBunkerKey(value); if (isValid) { formatHint.textContent = '✅ Valid bunker connection format detected'; formatHint.style.color = '#059669'; // Enable button connectButton.disabled = false; connectButton.style.borderColor = 'var(--nl-primary-color)'; connectButton.style.color = 'var(--nl-primary-color)'; connectButton.style.cursor = 'pointer'; } else { formatHint.textContent = '❌ Invalid format - must be bunker://, npub, or 64-char hex'; formatHint.style.color = '#dc2626'; // Disable button connectButton.disabled = true; connectButton.style.borderColor = 'var(--nl-muted-color)'; connectButton.style.color = 'var(--nl-muted-color)'; connectButton.style.cursor = 'not-allowed'; } }; const backButton = document.createElement('button'); backButton.textContent = 'Back'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; formGroup.appendChild(label); formGroup.appendChild(pubkeyInput); formGroup.appendChild(formatHint); this.modalBody.appendChild(description); this.modalBody.appendChild(formGroup); this.modalBody.appendChild(connectButton); this.modalBody.appendChild(backButton); } _validateBunkerKey(bunkerKey) { try { const trimmed = bunkerKey.trim(); // Check for bunker:// format if (trimmed.startsWith('bunker://')) { // Should have format: bunker://pubkey or bunker://pubkey?param=value const match = trimmed.match(/^bunker:\/\/([0-9a-fA-F]{64})(\?.*)?$/); return !!match; } // Check for npub format if (trimmed.startsWith('npub1') && trimmed.length === 63) { try { if (window.NostrTools?.nip19) { const decoded = window.NostrTools.nip19.decode(trimmed); return decoded.type === 'npub'; } } catch { return false; } } // Check for hex format (64 characters, valid hex) if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) { return true; } return false; } catch (error) { console.log('Bunker key validation failed:', error.message); return false; } } _handleNip46Connect(bunkerPubkey) { if (!bunkerPubkey || !bunkerPubkey.length) { this._showError('Bunker pubkey is required'); return; } this._showNip46Connecting(bunkerPubkey); this._performNip46Connect(bunkerPubkey); } _showNip46Connecting(bunkerPubkey) { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Connecting to Remote Signer...'; title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;'; const description = document.createElement('p'); description.textContent = 'Establishing secure connection to your remote signer.'; description.style.cssText = 'margin-bottom: 20px; color: #6b7280;'; // Normalize bunker pubkey for display (= show original format if bunker: prefix) const displayPubkey = bunkerPubkey.startsWith('bunker:') || bunkerPubkey.startsWith('npub') || bunkerPubkey.length === 64 ? bunkerPubkey : bunkerPubkey; const bunkerInfo = document.createElement('div'); bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;'; bunkerInfo.innerHTML = ` Connecting to bunker:
Connection: ${displayPubkey}
Connection string contains all necessary relay information. `; const connectingDiv = document.createElement('div'); connectingDiv.style.cssText = 'text-align: center; color: #6b7280;'; connectingDiv.innerHTML = `
Please wait while we establish the connection...
This may take a few seconds
`; this.modalBody.appendChild(title); this.modalBody.appendChild(description); this.modalBody.appendChild(bunkerInfo); this.modalBody.appendChild(connectingDiv); } async _performNip46Connect(bunkerPubkey) { try { console.log('Starting NIP-46 connection to bunker:', bunkerPubkey); // Check if nostr-tools NIP-46 is available if (!window.NostrTools?.nip46) { throw new Error('nostr-tools NIP-46 module not available'); } // Use nostr-tools to parse bunker input - this handles all formats correctly console.log('Parsing bunker input with nostr-tools...'); const bunkerPointer = await window.NostrTools.nip46.parseBunkerInput(bunkerPubkey); if (!bunkerPointer) { throw new Error('Unable to parse bunker connection string or resolve NIP-05 identifier'); } console.log('Parsed bunker pointer:', bunkerPointer); // Create local client keypair for this session const localSecretKey = window.NostrTools.generateSecretKey(); console.log('Generated local client keypair for NIP-46 session'); // Use nostr-tools BunkerSigner factory method (not constructor - it's private) console.log('Creating nip46 BunkerSigner...'); const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, { onauth: (url) => { console.log('Received auth URL from bunker:', url); // Open auth URL in popup or redirect window.open(url, '_blank', 'width=600,height=800'); } }); console.log('NIP-46 BunkerSigner created successfully'); // Skip ping test - NIP-46 works through relays, not direct connection // Try to connect directly (this may trigger auth flow) console.log('Attempting NIP-46 connect...'); await signer.connect(); console.log('NIP-46 connect successful'); // Get the user's public key from the bunker console.log('Getting public key from bunker...'); const userPubkey = await signer.getPublicKey(); console.log('NIP-46 user public key:', userPubkey); // Store the NIP-46 authentication info const nip46Info = { pubkey: userPubkey, signer: { method: 'nip46', remotePubkey: bunkerPointer.pubkey, bunkerSigner: signer, secret: bunkerPointer.secret, relays: bunkerPointer.relays } }; console.log('NOSTR_LOGIN_LITE NIP-46 connection established successfully!'); // Set as current auth method this._setAuthMethod('nip46', nip46Info); return; } catch (error) { console.error('NIP-46 connection failed:', error); this._showNip46Error(error.message); } } _showNip46Error(message) { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Connection Failed'; title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #dc2626;'; const errorMsg = document.createElement('p'); errorMsg.textContent = `Unable to connect to remote signer: ${message}`; errorMsg.style.cssText = 'margin-bottom: 20px; color: #6b7280;'; const retryButton = document.createElement('button'); retryButton.textContent = 'Try Again'; retryButton.onclick = () => this._showConnectScreen(); retryButton.style.cssText = this._getButtonStyle(); const backButton = document.createElement('button'); backButton.textContent = 'Back to Options'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; this.modalBody.appendChild(title); this.modalBody.appendChild(errorMsg); this.modalBody.appendChild(retryButton); this.modalBody.appendChild(backButton); } _handleReadonly() { // Set read-only mode this._setAuthMethod('readonly'); } _showSeedPhraseScreen() { this.modalBody.innerHTML = ''; const description = document.createElement('p'); description.innerHTML = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts, or generate new.'; description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;'; const textarea = document.createElement('textarea'); // Remove default placeholder text as requested textarea.placeholder = ''; textarea.style.cssText = ` width: 100%; height: 100px; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 12px; resize: none; font-family: monospace; font-size: 14px; box-sizing: border-box; `; // Add real-time mnemonic validation const formatHint = document.createElement('div'); formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;'; const importButton = document.createElement('button'); importButton.textContent = 'Import Accounts'; importButton.disabled = true; importButton.onclick = () => { if (!importButton.disabled) { this._importFromSeedPhrase(textarea.value); } }; // Set initial disabled state importButton.style.cssText = ` display: block; width: 100%; padding: 12px; border: var(--nl-border-width) solid var(--nl-muted-color); border-radius: var(--nl-border-radius); font-size: 16px; font-weight: 500; cursor: not-allowed; transition: all 0.2s; font-family: var(--nl-font-family, 'Courier New', monospace); background: var(--nl-secondary-color); color: var(--nl-muted-color); `; textarea.oninput = () => { const value = textarea.value.trim(); if (!value) { formatHint.textContent = ''; // Disable button importButton.disabled = true; importButton.style.borderColor = 'var(--nl-muted-color)'; importButton.style.color = 'var(--nl-muted-color)'; importButton.style.cursor = 'not-allowed'; return; } const isValid = this._validateMnemonic(value); if (isValid) { const wordCount = value.split(/\s+/).length; formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`; formatHint.style.color = '#059669'; // Enable button importButton.disabled = false; importButton.style.borderColor = 'var(--nl-primary-color)'; importButton.style.color = 'var(--nl-primary-color)'; importButton.style.cursor = 'pointer'; } else { formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words'; formatHint.style.color = '#dc2626'; // Disable button importButton.disabled = true; importButton.style.borderColor = 'var(--nl-muted-color)'; importButton.style.color = 'var(--nl-muted-color)'; importButton.style.cursor = 'not-allowed'; } }; const backButton = document.createElement('button'); backButton.textContent = 'Back'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;'; this.modalBody.appendChild(description); this.modalBody.appendChild(textarea); this.modalBody.appendChild(formatHint); this.modalBody.appendChild(importButton); this.modalBody.appendChild(backButton); // Add click handler for the "generate new" link const generateLink = document.getElementById('generate-new'); if (generateLink) { generateLink.addEventListener('mouseenter', () => { generateLink.style.color = 'var(--nl-accent-color)'; }); generateLink.addEventListener('mouseleave', () => { generateLink.style.color = 'var(--nl-primary-color)'; }); generateLink.addEventListener('click', () => { this._generateNewSeedPhrase(textarea, formatHint); }); } } _generateNewSeedPhrase(textarea, formatHint) { try { // Check if NIP-06 is available if (!window.NostrTools?.nip06) { throw new Error('NIP-06 not available in bundle'); } // Generate a random 12-word mnemonic using NostrTools const mnemonic = window.NostrTools.nip06.generateSeedWords(); // Set the generated mnemonic in the textarea textarea.value = mnemonic; // Trigger the oninput event to properly validate and enable the button if (textarea.oninput) { textarea.oninput(); } console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words'); } catch (error) { console.error('Failed to generate seed phrase:', error); formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available'; formatHint.style.color = '#dc2626'; } } _validateMnemonic(mnemonic) { try { // Check if NIP-06 is available if (!window.NostrTools?.nip06) { console.error('NIP-06 not available in bundle'); return false; } const words = mnemonic.trim().split(/\s+/); // Must be 12 or 24 words if (words.length !== 12 && words.length !== 24) { return false; } // Try to validate using NostrTools nip06 - this will throw if invalid window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0); return true; } catch (error) { console.log('Mnemonic validation failed:', error.message); return false; } } _importFromSeedPhrase(mnemonic) { try { const trimmed = mnemonic.trim(); if (!trimmed) { throw new Error('Please enter a mnemonic seed phrase'); } // Validate the mnemonic if (!this._validateMnemonic(trimmed)) { throw new Error('Invalid mnemonic. Please enter a valid 12 or 24-word BIP-39 seed phrase'); } // Generate accounts 0-5 using NIP-06 const accounts = []; for (let i = 0; i < 6; i++) { try { const privateKey = window.NostrTools.nip06.privateKeyFromSeedWords(trimmed, '', i); const publicKey = window.NostrTools.getPublicKey(privateKey); const nsec = window.NostrTools.nip19.nsecEncode(privateKey); const npub = window.NostrTools.nip19.npubEncode(publicKey); accounts.push({ index: i, privateKey, publicKey, nsec, npub }); } catch (error) { console.error(`Failed to derive account ${i}:`, error); } } if (accounts.length === 0) { throw new Error('Failed to derive any accounts from seed phrase'); } console.log(`Successfully derived ${accounts.length} accounts from seed phrase`); this._showAccountSelection(accounts); } catch (error) { console.error('Seed phrase import failed:', error); this._showError('Seed phrase import failed: ' + error.message); } } _showAccountSelection(accounts) { this.modalBody.innerHTML = ''; const description = document.createElement('p'); description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`; description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;'; this.modalBody.appendChild(description); // Create table for account selection const table = document.createElement('table'); table.style.cssText = ` width: 100%; border-collapse: collapse; margin-bottom: 20px; font-family: var(--nl-font-family, 'Courier New', monospace); font-size: 12px; `; // Table header const thead = document.createElement('thead'); thead.innerHTML = ` # Use `; table.appendChild(thead); // Table body const tbody = document.createElement('tbody'); accounts.forEach(account => { const row = document.createElement('tr'); row.style.cssText = 'border: 1px solid #d1d5db;'; const indexCell = document.createElement('td'); indexCell.textContent = account.index; indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;'; const actionCell = document.createElement('td'); actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;'; // Show truncated npub in the button const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`; const selectButton = document.createElement('button'); selectButton.textContent = truncatedNpub; selectButton.onclick = () => this._selectAccount(account); selectButton.style.cssText = ` width: 100%; padding: 8px 12px; font-size: 11px; background: var(--nl-secondary-color); color: var(--nl-primary-color); border: 1px solid var(--nl-primary-color); border-radius: 4px; cursor: pointer; font-family: 'Courier New', monospace; text-align: center; `; selectButton.onmouseover = () => { selectButton.style.borderColor = 'var(--nl-accent-color)'; }; selectButton.onmouseout = () => { selectButton.style.borderColor = 'var(--nl-primary-color)'; }; actionCell.appendChild(selectButton); row.appendChild(indexCell); row.appendChild(actionCell); tbody.appendChild(row); }); table.appendChild(tbody); this.modalBody.appendChild(table); // Back button const backButton = document.createElement('button'); backButton.textContent = 'Back to Seed Phrase'; backButton.onclick = () => this._showSeedPhraseScreen(); backButton.style.cssText = this._getButtonStyle('secondary'); this.modalBody.appendChild(backButton); } _selectAccount(account) { console.log('Selected account:', account.index, account.npub); // Use the same auth method as local keys, but with seedphrase identifier this._setAuthMethod('local', { secret: account.nsec, pubkey: account.publicKey, source: 'seedphrase', accountIndex: account.index }); } _showOtpScreen() { // Placeholder for OTP functionality this._showError('OTP/DM not yet implemented - coming soon!'); } _getButtonStyle(type = 'primary') { const baseStyle = ` display: block; width: 100%; padding: 12px; border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); font-size: 16px; font-weight: 500; cursor: pointer; transition: all 0.2s; font-family: var(--nl-font-family, 'Courier New', monospace); `; if (type === 'primary') { return baseStyle + ` background: var(--nl-secondary-color); color: var(--nl-primary-color); `; } else { return baseStyle + ` background: #cccccc; color: var(--nl-primary-color); `; } } // Public API static init(options) { if (Modal.instance) return Modal.instance; Modal.instance = new Modal(options); return Modal.instance; } static getInstance() { return Modal.instance; } } // Initialize global instance let modalInstance = null; window.addEventListener('load', () => { modalInstance = new Modal(); }); // ====================================== // FloatingTab Component (Recovered from git history) // ====================================== class FloatingTab { constructor(modal, options = {}) { this.modal = modal; this.options = { enabled: true, hPosition: 1.0, // 0.0 = left, 1.0 = right vPosition: 0.5, // 0.0 = top, 1.0 = bottom offset: { x: 0, y: 0 }, appearance: { style: 'pill', // 'pill', 'square', 'circle' theme: 'auto', // 'auto', 'light', 'dark' icon: '', text: 'Login', iconOnly: false }, behavior: { hideWhenAuthenticated: true, showUserInfo: true, autoSlide: true, persistent: false }, getUserInfo: false, getUserRelay: [], ...options }; this.userProfile = null; this.container = null; this.isVisible = false; if (this.options.enabled) { this._init(); } } _init() { console.log('FloatingTab: Initializing with options:', this.options); this._createContainer(); this._setupEventListeners(); this._updateAppearance(); this._position(); this.show(); } // Get authentication state from authoritative source (Global Storage-Based Function) _getAuthState() { return window.NOSTR_LOGIN_LITE?.getAuthState?.() || null; } _createContainer() { // Remove existing floating tab if any const existingTab = document.getElementById('nl-floating-tab'); if (existingTab) { existingTab.remove(); } this.container = document.createElement('div'); this.container.id = 'nl-floating-tab'; this.container.className = 'nl-floating-tab'; // Base styles - positioning and behavior this.container.style.cssText = ` position: fixed; z-index: 9999; cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; font-size: 14px; font-weight: 500; padding: 8px 16px; min-width: 80px; max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; document.body.appendChild(this.container); } _setupEventListeners() { if (!this.container) return; // Click handler this.container.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this._handleClick(); }); // Hover effects this.container.addEventListener('mouseenter', () => { if (this.options.behavior.autoSlide) { this._slideIn(); } }); this.container.addEventListener('mouseleave', () => { if (this.options.behavior.autoSlide) { this._slideOut(); } }); // Listen for authentication events window.addEventListener('nlMethodSelected', (e) => { console.log('🔍 FloatingTab: Authentication method selected event received'); console.log('🔍 FloatingTab: Event detail:', e.detail); this._handleAuth(e.detail); }); window.addEventListener('nlAuthRestored', (e) => { console.log('🔍 FloatingTab: ✅ Authentication restored event received'); console.log('🔍 FloatingTab: Event detail:', e.detail); console.log('🔍 FloatingTab: Calling _handleAuth with restored data...'); this._handleAuth(e.detail); }); window.addEventListener('nlLogout', () => { console.log('🔍 FloatingTab: Logout event received'); this._handleLogout(); }); // Check for existing authentication state on initialization window.addEventListener('load', () => { setTimeout(() => { this._checkExistingAuth(); }, 1000); // Wait 1 second for all initialization to complete }); } // Check for existing authentication on page load async _checkExistingAuth() { console.log('🔍 FloatingTab: === _checkExistingAuth START ==='); try { const storageKey = 'nostr_login_lite_auth'; let storedAuth = null; // Try sessionStorage first, then localStorage if (sessionStorage.getItem(storageKey)) { storedAuth = JSON.parse(sessionStorage.getItem(storageKey)); console.log('🔍 FloatingTab: Found auth in sessionStorage:', storedAuth.method); } else if (localStorage.getItem(storageKey)) { storedAuth = JSON.parse(localStorage.getItem(storageKey)); console.log('🔍 FloatingTab: Found auth in localStorage:', storedAuth.method); } if (storedAuth) { // Check if stored auth is not expired const maxAge = storedAuth.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000; if (Date.now() - storedAuth.timestamp <= maxAge) { console.log('🔍 FloatingTab: Found valid stored auth, simulating auth event'); // Create auth data object for FloatingTab const authData = { method: storedAuth.method, pubkey: storedAuth.pubkey }; // For extensions, try to find the extension if (storedAuth.method === 'extension') { if (window.nostr && window.nostr.constructor?.name !== 'WindowNostr') { authData.extension = window.nostr; } } await this._handleAuth(authData); } else { console.log('🔍 FloatingTab: Stored auth expired, clearing'); sessionStorage.removeItem(storageKey); localStorage.removeItem(storageKey); } } else { console.log('🔍 FloatingTab: No existing authentication found'); } } catch (error) { console.error('🔍 FloatingTab: Error checking existing auth:', error); } console.log('🔍 FloatingTab: === _checkExistingAuth END ==='); } _handleClick() { console.log('FloatingTab: Clicked'); const authState = this._getAuthState(); if (authState && this.options.behavior.showUserInfo) { // Show user menu or profile options this._showUserMenu(); } else { // Always open login modal (consistent with login buttons) if (this.modal) { this.modal.open({ startScreen: 'login' }); } } } // Check if object is a real extension (same logic as NostrLite._isRealExtension) _isRealExtension(obj) { if (!obj || typeof obj !== 'object') { return false; } // Must have required Nostr methods if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { return false; } // Exclude our own library classes const constructorName = obj.constructor?.name; if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') { return false; } // Exclude NostrTools library object if (obj === window.NostrTools) { return false; } // Conservative check: Look for common extension characteristics const extensionIndicators = [ '_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope', '_requests', '_pubkey', 'name', 'version', 'description' ]; const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop)); // Additional check: Extensions often have specific constructor patterns const hasExtensionConstructor = constructorName && constructorName !== 'Object' && constructorName !== 'Function'; return hasIndicators || hasExtensionConstructor; } // Try to login with extension and trigger proper persistence async _tryExtensionLogin(extension) { try { console.log('FloatingTab: Attempting extension login'); // Get pubkey from extension const pubkey = await extension.getPublicKey(); console.log('FloatingTab: Extension provided pubkey:', pubkey); // Create extension auth data const extensionAuth = { method: 'extension', pubkey: pubkey, extension: extension }; // **CRITICAL FIX**: Dispatch nlMethodSelected event to trigger persistence console.log('FloatingTab: Dispatching nlMethodSelected for persistence'); if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('nlMethodSelected', { detail: extensionAuth })); } // Also call our local _handleAuth for UI updates await this._handleAuth(extensionAuth); } catch (error) { console.error('FloatingTab: Extension login failed:', error); // Fall back to opening modal if (this.modal) { this.modal.open({ startScreen: 'login' }); } } } async _handleAuth(authData) { console.log('🔍 FloatingTab: === _handleAuth START ==='); console.log('🔍 FloatingTab: authData received:', authData); // Wait a brief moment for WindowNostr to process the authentication setTimeout(async () => { console.log('🔍 FloatingTab: Checking authentication state from authoritative source...'); const authState = this._getAuthState(); const isAuthenticated = !!authState; console.log('🔍 FloatingTab: Authoritative auth state:', authState); console.log('🔍 FloatingTab: Is authenticated:', isAuthenticated); if (isAuthenticated) { console.log('🔍 FloatingTab: ✅ Authentication verified from authoritative source'); } else { console.error('🔍 FloatingTab: ❌ Authentication not found in authoritative source'); } // Fetch user profile if enabled and we have a pubkey if (this.options.getUserInfo && authData.pubkey) { console.log('🔍 FloatingTab: getUserInfo enabled, fetching profile for:', authData.pubkey); try { const profile = await this._fetchUserProfile(authData.pubkey); this.userProfile = profile; console.log('🔍 FloatingTab: User profile fetched:', profile); } catch (error) { console.warn('🔍 FloatingTab: Failed to fetch user profile:', error); this.userProfile = null; } } else { console.log('🔍 FloatingTab: getUserInfo disabled or no pubkey, skipping profile fetch'); } this._updateAppearance(); // Update UI based on authoritative state console.log('🔍 FloatingTab: hideWhenAuthenticated option:', this.options.behavior.hideWhenAuthenticated); if (this.options.behavior.hideWhenAuthenticated && isAuthenticated) { console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true and authenticated)'); this.hide(); } else { console.log('🔍 FloatingTab: Keeping tab visible'); } }, 500); // Wait 500ms for WindowNostr to complete authentication processing console.log('🔍 FloatingTab: === _handleAuth END ==='); } _handleLogout() { console.log('FloatingTab: Handling logout'); this.userProfile = null; if (this.options.behavior.hideWhenAuthenticated) { this.show(); } this._updateAppearance(); } _showUserMenu() { // Simple user menu - could be expanded const menu = document.createElement('div'); menu.style.cssText = ` position: fixed; background: var(--nl-secondary-color); border: var(--nl-border-width) solid var(--nl-primary-color); border-radius: var(--nl-border-radius); padding: 12px; z-index: 10000; font-family: var(--nl-font-family); box-shadow: 0 4px 12px rgba(0,0,0,0.15); `; // Position near the floating tab const tabRect = this.container.getBoundingClientRect(); if (this.options.hPosition > 0.5) { // Tab is on right side, show menu to the left menu.style.right = (window.innerWidth - tabRect.left) + 'px'; } else { // Tab is on left side, show menu to the right menu.style.left = tabRect.right + 'px'; } menu.style.top = tabRect.top + 'px'; // Menu content - use _getAuthState() as single source of truth const authState = this._getAuthState(); let userDisplay; if (authState?.pubkey) { // 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; } else { userDisplay = `${authState.pubkey.slice(0, 8)}...${authState.pubkey.slice(-4)}`; } } else { userDisplay = 'Authenticated'; } menu.innerHTML = `
${userDisplay}
`; document.body.appendChild(menu); // Auto-remove menu after delay or on outside click const removeMenu = () => menu.remove(); setTimeout(removeMenu, 5000); document.addEventListener('click', function onOutsideClick(e) { if (!menu.contains(e.target) && e.target !== this.container) { removeMenu(); document.removeEventListener('click', onOutsideClick); } }); } _updateAppearance() { if (!this.container) return; // Query authoritative source for all state information const authState = this._getAuthState(); const isAuthenticated = authState !== null; // Update content if (isAuthenticated && this.options.behavior.showUserInfo) { let display; // Use profile name if available, otherwise fall back to pubkey if (this.userProfile?.name || this.userProfile?.display_name) { const userName = this.userProfile.name || this.userProfile.display_name; display = this.options.appearance.iconOnly ? userName.slice(0, 8) : userName; } else if (authState?.pubkey) { // Fallback to pubkey display display = this.options.appearance.iconOnly ? authState.pubkey.slice(0, 6) : `${authState.pubkey.slice(0, 6)}...`; } else { display = this.options.appearance.iconOnly ? 'User' : 'Authenticated'; } this.container.textContent = display; this.container.className = 'nl-floating-tab nl-floating-tab--logged-in'; } 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.container.textContent = display; this.container.className = 'nl-floating-tab nl-floating-tab--logged-out'; } // Apply appearance styles based on current state this._applyThemeStyles(); } _applyThemeStyles() { if (!this.container) return; // The CSS classes will handle the theming through CSS custom properties // Additional style customizations can be added here if needed // Apply style variant if (this.options.appearance.style === 'circle') { this.container.style.borderRadius = '50%'; this.container.style.width = '48px'; this.container.style.height = '48px'; this.container.style.minWidth = '48px'; this.container.style.padding = '0'; } else if (this.options.appearance.style === 'square') { this.container.style.borderRadius = '4px'; } else { // pill style (default) this.container.style.borderRadius = 'var(--nl-border-radius)'; } } async _fetchUserProfile(pubkey) { if (!this.options.getUserInfo) { console.log('FloatingTab: getUserInfo disabled, skipping profile fetch'); return null; } // Determine which relays to use const relays = this.options.getUserRelay.length > 0 ? this.options.getUserRelay : ['wss://relay.damus.io', 'wss://nos.lol']; console.log('FloatingTab: Fetching profile from relays:', relays); try { // Create a SimplePool instance for querying const pool = new window.NostrTools.SimplePool(); // Query for kind 0 (user metadata) events const events = await pool.querySync(relays, { kinds: [0], authors: [pubkey], limit: 1 }, { timeout: 5000 }); console.log('FloatingTab: Profile query returned', events.length, 'events'); if (events.length === 0) { console.log('FloatingTab: No profile events found'); return null; } // Get the most recent event const latestEvent = events.sort((a, b) => b.created_at - a.created_at)[0]; try { const profile = JSON.parse(latestEvent.content); console.log('FloatingTab: Parsed profile:', profile); // Find the best name from any key containing "name" (case-insensitive) let bestName = null; const nameKeys = Object.keys(profile).filter(key => key.toLowerCase().includes('name') && typeof profile[key] === 'string' && profile[key].trim().length > 0 ); if (nameKeys.length > 0) { // Find the shortest name value bestName = nameKeys .map(key => profile[key].trim()) .reduce((shortest, current) => current.length < shortest.length ? current : shortest ); console.log('FloatingTab: Found name keys:', nameKeys, 'selected:', bestName); } // Return relevant profile fields with the best name return { name: bestName, display_name: profile.display_name || null, about: profile.about || null, picture: profile.picture || null, nip05: profile.nip05 || null }; } catch (parseError) { console.warn('FloatingTab: Failed to parse profile JSON:', parseError); return null; } } catch (error) { console.error('FloatingTab: Profile fetch error:', error); return null; } } _position() { if (!this.container) return; const padding = 16; // Distance from screen edge // Calculate position based on percentage 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`; console.log(`FloatingTab: Positioned at (${x}, ${y})`); } _slideIn() { if (!this.container || !this.options.behavior.autoSlide) return; // Slide towards center slightly const currentTransform = this.container.style.transform || ''; if (this.options.hPosition > 0.5) { this.container.style.transform = currentTransform + ' translateX(-8px)'; } else { this.container.style.transform = currentTransform + ' translateX(8px)'; } } _slideOut() { if (!this.container || !this.options.behavior.autoSlide) return; // Reset position this.container.style.transform = ''; } show() { if (!this.container) return; this.container.style.display = 'flex'; this.isVisible = true; console.log('FloatingTab: Shown'); } hide() { if (!this.container) return; this.container.style.display = 'none'; this.isVisible = false; console.log('FloatingTab: Hidden'); } destroy() { if (this.container) { this.container.remove(); this.container = null; } this.isVisible = false; console.log('FloatingTab: Destroyed'); } // Update options and re-apply updateOptions(newOptions) { this.options = { ...this.options, ...newOptions }; if (this.container) { this._updateAppearance(); this._position(); } } // Get current state getState() { const authState = this._getAuthState(); return { isVisible: this.isVisible, isAuthenticated: !!authState, userInfo: authState, options: this.options }; } } // ====================================== // Main NOSTR_LOGIN_LITE Library // ====================================== // Extension Bridge for managing browser extensions class ExtensionBridge { constructor() { this.extensions = new Map(); this.primaryExtension = null; this._detectExtensions(); } _detectExtensions() { // Common extension locations const locations = [ { path: 'window.nostr', name: 'Generic' }, { path: 'window.alby?.nostr', name: 'Alby' }, { path: 'window.nos2x?.nostr', name: 'nos2x' }, { path: 'window.flamingo?.nostr', name: 'Flamingo' }, { path: 'window.getAlby?.nostr', name: 'Alby Legacy' }, { path: 'window.mutiny?.nostr', name: 'Mutiny' } ]; for (const location of locations) { try { const obj = eval(location.path); if (obj && typeof obj.getPublicKey === 'function') { this.extensions.set(location.name, { name: location.name, extension: obj, constructor: obj.constructor?.name || 'Unknown' }); if (!this.primaryExtension) { this.primaryExtension = this.extensions.get(location.name); } } } catch (e) { // Extension not available } } } getAllExtensions() { return Array.from(this.extensions.values()); } getExtensionCount() { return this.extensions.size; } } // Main NostrLite class class NostrLite { constructor() { this.options = {}; this.extensionBridge = new ExtensionBridge(); this.initialized = false; this.currentTheme = 'default'; this.modal = null; this.floatingTab = null; } async init(options = {}) { console.log('NOSTR_LOGIN_LITE: Initializing with options:', options); this.options = { theme: 'default', persistence: true, // Enable persistent authentication by default isolateSession: false, // Use localStorage by default for cross-window persistence methods: { extension: true, local: true, seedphrase: false, readonly: true, connect: false, otp: false }, floatingTab: { enabled: false, hPosition: 1.0, vPosition: 0.5, offset: { x: 0, y: 0 }, appearance: { style: 'pill', theme: 'auto', icon: '', text: 'Login', iconOnly: false }, behavior: { hideWhenAuthenticated: true, showUserInfo: true, autoSlide: true, persistent: false }, getUserInfo: false, getUserRelay: [] }, ...options }; // Apply the selected theme (CSS-only) this.switchTheme(this.options.theme); // Always set up window.nostr facade to handle multiple extensions properly console.log('🔍 NOSTR_LOGIN_LITE: Setting up facade before other initialization...'); await this._setupWindowNostrFacade(); console.log('🔍 NOSTR_LOGIN_LITE: Facade setup complete, continuing initialization...'); // Create modal during init (matching original git architecture) this.modal = new Modal(this.options); console.log('NOSTR_LOGIN_LITE: Modal created during init'); // Initialize floating tab if enabled if (this.options.floatingTab.enabled) { this.floatingTab = new FloatingTab(this.modal, this.options.floatingTab); console.log('NOSTR_LOGIN_LITE: Floating tab initialized'); } // Attempt to restore authentication state if persistence is enabled (AFTER facade is ready) if (this.options.persistence) { console.log('🔍 NOSTR_LOGIN_LITE: Persistence enabled, attempting auth restoration...'); await this._attemptAuthRestore(); } else { console.log('🔍 NOSTR_LOGIN_LITE: Persistence disabled in options'); } this.initialized = true; console.log('NOSTR_LOGIN_LITE: Initialization complete'); return this; } async _setupWindowNostrFacade() { if (typeof window !== 'undefined') { console.log('🔍 NOSTR_LOGIN_LITE: === EXTENSION-FIRST FACADE SETUP ==='); console.log('🔍 NOSTR_LOGIN_LITE: Current window.nostr:', window.nostr); console.log('🔍 NOSTR_LOGIN_LITE: Constructor:', window.nostr?.constructor?.name); // EXTENSION-FIRST ARCHITECTURE: Never interfere with real extensions if (this._isRealExtension(window.nostr)) { console.log('🔍 NOSTR_LOGIN_LITE: ✅ REAL EXTENSION DETECTED - WILL NOT INSTALL FACADE'); console.log('🔍 NOSTR_LOGIN_LITE: Extension constructor:', window.nostr.constructor?.name); console.log('🔍 NOSTR_LOGIN_LITE: Extensions will handle window.nostr directly'); // Store reference for persistence verification this.detectedExtension = window.nostr; this.hasExtension = true; this.facadeInstalled = false; // We deliberately don't install facade for extensions console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - no facade interference'); return; // Don't install facade at all for extensions } // NO EXTENSION: Install facade for local/NIP-46/readonly methods console.log('🔍 NOSTR_LOGIN_LITE: ❌ No real extension detected'); console.log('🔍 NOSTR_LOGIN_LITE: Installing facade for non-extension authentication'); this.hasExtension = false; this._installFacade(window.nostr); // Install facade with any existing nostr object console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade installed for local/NIP-46/readonly methods'); } } _installFacade(existingNostr = null) { if (typeof window !== 'undefined' && !this.facadeInstalled) { 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); const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession }); window.nostr = facade; this.facadeInstalled = true; console.log('🔍 NOSTR_LOGIN_LITE: === FACADE INSTALLED FOR PERSISTENCE ==='); 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); } } // Conservative method to identify real browser extensions _isRealExtension(obj) { console.log('NOSTR_LOGIN_LITE: === _isRealExtension (Conservative) ==='); console.log('NOSTR_LOGIN_LITE: obj:', obj); console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj); if (!obj || typeof obj !== 'object') { console.log('NOSTR_LOGIN_LITE: ✗ Not an object'); return false; } // Must have required Nostr methods if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { console.log('NOSTR_LOGIN_LITE: ✗ Missing required NIP-07 methods'); return false; } // Exclude our own library classes const constructorName = obj.constructor?.name; console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName); if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') { console.log('NOSTR_LOGIN_LITE: ✗ Is our library class - NOT an extension'); return false; } // Exclude NostrTools library object if (obj === window.NostrTools) { console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object - NOT an extension'); return false; } // Conservative check: Look for common extension characteristics // Real extensions usually have some of these internal properties const extensionIndicators = [ '_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope', '_requests', '_pubkey', 'name', 'version', 'description' ]; const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop)); // Additional check: Extensions often have specific constructor patterns const hasExtensionConstructor = constructorName && constructorName !== 'Object' && constructorName !== 'Function'; const isExtension = hasIndicators || hasExtensionConstructor; console.log('NOSTR_LOGIN_LITE: Extension indicators found:', hasIndicators); console.log('NOSTR_LOGIN_LITE: Has extension constructor:', hasExtensionConstructor); console.log('NOSTR_LOGIN_LITE: Final result for', constructorName, ':', isExtension); return isExtension; } launch(startScreen = 'login') { console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen); if (this.modal) { this.modal.open({ startScreen }); } else { console.error('NOSTR_LOGIN_LITE: Modal not initialized - call init() first'); } } // Attempt to restore authentication state async _attemptAuthRestore() { try { console.log('🔍 NOSTR_LOGIN_LITE: === _attemptAuthRestore START ==='); console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension); console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled); console.log('🔍 NOSTR_LOGIN_LITE: window.nostr:', window.nostr?.constructor?.name); if (this.hasExtension) { // EXTENSION MODE: Use custom extension persistence logic console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - using extension-specific restore'); const restoredAuth = await this._attemptExtensionRestore(); if (restoredAuth) { console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth restored successfully!'); return restoredAuth; } else { console.log('🔍 NOSTR_LOGIN_LITE: ❌ Extension auth could not be restored'); return null; } } else if (this.facadeInstalled && window.nostr?.restoreAuthState) { // NON-EXTENSION MODE: Use facade persistence logic console.log('🔍 NOSTR_LOGIN_LITE: Non-extension mode - using facade restore'); const restoredAuth = await window.nostr.restoreAuthState(); if (restoredAuth) { console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade auth restored successfully!'); console.log('🔍 NOSTR_LOGIN_LITE: Method:', restoredAuth.method); console.log('🔍 NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); // Handle NIP-46 reconnection requirement if (restoredAuth.requiresReconnection) { console.log('🔍 NOSTR_LOGIN_LITE: NIP-46 connection requires user reconnection'); this._showReconnectionPrompt(restoredAuth); } return restoredAuth; } else { console.log('🔍 NOSTR_LOGIN_LITE: ❌ Facade auth could not be restored'); return null; } } else { console.log('🔍 NOSTR_LOGIN_LITE: ❌ No restoration method available'); console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension); console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled); console.log('🔍 NOSTR_LOGIN_LITE: window.nostr.restoreAuthState:', typeof window.nostr?.restoreAuthState); return null; } } catch (error) { console.error('🔍 NOSTR_LOGIN_LITE: Auth restoration failed with error:', error); console.error('🔍 NOSTR_LOGIN_LITE: Error stack:', error.stack); return null; } } // Extension-specific authentication restoration async _attemptExtensionRestore() { try { console.log('🔍 NOSTR_LOGIN_LITE: === _attemptExtensionRestore START ==='); // Use a simple AuthManager instance for extension persistence const authManager = new AuthManager({ isolateSession: this.options?.isolateSession }); const storedAuth = await authManager.restoreAuthState(); if (!storedAuth || storedAuth.method !== 'extension') { console.log('🔍 NOSTR_LOGIN_LITE: No extension auth state stored'); return null; } // Verify the extension is still available and working if (!window.nostr || !this._isRealExtension(window.nostr)) { console.log('🔍 NOSTR_LOGIN_LITE: Extension no longer available'); authManager.clearAuthState(); // Clear invalid state return null; } try { // Test that the extension still works with the same pubkey const currentPubkey = await window.nostr.getPublicKey(); if (currentPubkey !== storedAuth.pubkey) { console.log('🔍 NOSTR_LOGIN_LITE: Extension pubkey changed, clearing state'); authManager.clearAuthState(); return null; } console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth verification successful'); // Create extension auth data for UI restoration const extensionAuth = { method: 'extension', pubkey: storedAuth.pubkey, extension: window.nostr }; // Dispatch restoration event so UI can update if (typeof window !== 'undefined') { console.log('🔍 NOSTR_LOGIN_LITE: Dispatching nlAuthRestored event for extension'); window.dispatchEvent(new CustomEvent('nlAuthRestored', { detail: extensionAuth })); } return extensionAuth; } catch (error) { console.log('🔍 NOSTR_LOGIN_LITE: Extension verification failed:', error); authManager.clearAuthState(); // Clear invalid state return null; } } catch (error) { console.error('🔍 NOSTR_LOGIN_LITE: Extension restore failed:', error); return null; } } // Show prompt for NIP-46 reconnection _showReconnectionPrompt(authData) { console.log('NOSTR_LOGIN_LITE: Showing reconnection prompt for NIP-46'); // Dispatch event that UI can listen to if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('nlReconnectionRequired', { detail: { method: authData.method, pubkey: authData.pubkey, connectionData: authData.connectionData, message: 'Your NIP-46 session has expired. Please reconnect to continue.' } })); } } logout() { console.log('NOSTR_LOGIN_LITE: Logout called'); // Clear legacy stored data if (typeof localStorage !== 'undefined') { localStorage.removeItem('nl_current'); } // Clear current authentication state directly from storage // This works for ALL methods including extensions (fixes the bug) clearAuthState(); // Dispatch logout event for UI updates if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('nlLogout', { detail: { timestamp: Date.now() } })); } } // CSS-only theme switching switchTheme(themeName) { console.log(`NOSTR_LOGIN_LITE: Switching to ${themeName} theme`); if (THEME_CSS[themeName]) { injectThemeCSS(themeName); this.currentTheme = themeName; // Dispatch theme change event if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('nlThemeChanged', { detail: { theme: themeName } })); } return { theme: themeName }; } else { console.warn(`Theme '${themeName}' not found, using default`); injectThemeCSS('default'); this.currentTheme = 'default'; return { theme: 'default' }; } } getCurrentTheme() { return this.currentTheme; } getAvailableThemes() { return Object.keys(THEME_CSS); } embed(container, options = {}) { console.log('NOSTR_LOGIN_LITE: Creating embedded modal in container:', container); const embedOptions = { ...this.options, ...options, embedded: container }; // Create new modal instance for embedding const embeddedModal = new Modal(embedOptions); embeddedModal.open(); return embeddedModal; } // Floating tab management methods showFloatingTab() { if (this.floatingTab) { this.floatingTab.show(); } else { console.warn('NOSTR_LOGIN_LITE: Floating tab not enabled'); } } hideFloatingTab() { if (this.floatingTab) { this.floatingTab.hide(); } } toggleFloatingTab() { if (this.floatingTab) { if (this.floatingTab.isVisible) { this.floatingTab.hide(); } else { this.floatingTab.show(); } } } updateFloatingTab(options) { if (this.floatingTab) { this.floatingTab.updateOptions(options); } } getFloatingTabState() { return this.floatingTab ? this.floatingTab.getState() : null; } } // ====================================== // Authentication Manager for Persistent Login // ====================================== // 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 class AuthManager { constructor(options = {}) { this.storageKey = 'nostr_login_lite_auth'; this.currentAuthState = null; // Configure storage type based on isolateSession option if (options.isolateSession) { this.storage = sessionStorage; console.log('AuthManager: Using sessionStorage for per-window isolation'); } else { this.storage = localStorage; console.log('AuthManager: Using localStorage for cross-window persistence'); } } // Save authentication state with method-specific security async saveAuthState(authData) { try { const authState = { method: authData.method, timestamp: Date.now(), pubkey: authData.pubkey }; 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' }; break; case 'local': // For local keys, encrypt the secret key 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); } break; case 'nip46': // For NIP-46, store connection parameters (no secrets) if (authData.signer) { authState.nip46 = { remotePubkey: authData.signer.remotePubkey, relays: authData.signer.relays, // Don't store secret - user will need to reconnect }; } break; case 'readonly': // Read-only mode has no secrets to store break; default: 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); } catch (error) { console.error('AuthManager: Failed to save auth state:', error); throw error; } } // Restore authentication state on page load async restoreAuthState() { try { console.log('🔍 AuthManager: === restoreAuthState START ==='); console.log('🔍 AuthManager: storageKey:', this.storageKey); const stored = this.storage.getItem(this.storageKey); console.log('🔍 AuthManager: localStorage raw value:', stored); if (!stored) { console.log('🔍 AuthManager: ❌ No stored auth state found'); return null; } const authState = JSON.parse(stored); console.log('🔍 AuthManager: ✅ Parsed stored auth state:', authState); console.log('🔍 AuthManager: Method:', authState.method); console.log('🔍 AuthManager: Timestamp:', authState.timestamp); console.log('🔍 AuthManager: Age (ms):', Date.now() - authState.timestamp); // Check if stored state is too old (24 hours for most methods, 1 hour for extensions) const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000; console.log('🔍 AuthManager: Max age for method:', maxAge, 'ms'); if (Date.now() - authState.timestamp > maxAge) { console.log('🔍 AuthManager: ❌ Stored auth state expired, clearing'); this.clearAuthState(); return null; } console.log('🔍 AuthManager: ✅ Auth state not expired, attempting restore for method:', authState.method); let result; switch (authState.method) { case 'extension': console.log('🔍 AuthManager: Calling _restoreExtensionAuth...'); result = await this._restoreExtensionAuth(authState); break; case 'local': console.log('🔍 AuthManager: Calling _restoreLocalAuth...'); result = await this._restoreLocalAuth(authState); break; case 'nip46': console.log('🔍 AuthManager: Calling _restoreNip46Auth...'); result = await this._restoreNip46Auth(authState); break; case 'readonly': console.log('🔍 AuthManager: Calling _restoreReadonlyAuth...'); result = await this._restoreReadonlyAuth(authState); break; default: console.warn('🔍 AuthManager: ❌ Unknown auth method in stored state:', authState.method); return null; } console.log('🔍 AuthManager: Restore method result:', result); console.log('🔍 AuthManager: === restoreAuthState END ==='); return result; } catch (error) { console.error('🔍 AuthManager: ❌ Failed to restore auth state:', error); console.error('🔍 AuthManager: Error stack:', error.stack); this.clearAuthState(); // Clear corrupted state return null; } } async _restoreExtensionAuth(authState) { console.log('🔍 AuthManager: === _restoreExtensionAuth START ==='); console.log('🔍 AuthManager: authState:', authState); console.log('🔍 AuthManager: window.nostr available:', !!window.nostr); console.log('🔍 AuthManager: window.nostr constructor:', window.nostr?.constructor?.name); // SMART EXTENSION WAITING SYSTEM // Extensions often load after our library, so we need to wait for them const extension = await this._waitForExtension(authState, 3000); // Wait up to 3 seconds if (!extension) { console.log('🔍 AuthManager: ❌ No extension found after waiting'); return null; } console.log('🔍 AuthManager: ✅ Extension found:', extension.constructor?.name); try { // Verify extension still works and has same pubkey const currentPubkey = await extension.getPublicKey(); if (currentPubkey !== authState.pubkey) { console.log('🔍 AuthManager: ❌ Extension pubkey changed, not restoring'); console.log('🔍 AuthManager: Expected:', authState.pubkey); console.log('🔍 AuthManager: Got:', currentPubkey); return null; } console.log('🔍 AuthManager: ✅ Extension auth restored successfully'); return { method: 'extension', pubkey: authState.pubkey, extension: extension }; } catch (error) { console.log('🔍 AuthManager: ❌ Extension verification failed:', error); return null; } } // Smart extension waiting system - polls multiple locations for extensions async _waitForExtension(authState, maxWaitMs = 3000) { console.log('🔍 AuthManager: === _waitForExtension START ==='); console.log('🔍 AuthManager: maxWaitMs:', maxWaitMs); console.log('🔍 AuthManager: Looking for extension with constructor:', authState.extensionVerification?.constructor); const startTime = Date.now(); const pollInterval = 100; // Check every 100ms // Extension locations to check (in priority order) const extensionLocations = [ { path: 'window.nostr', getter: () => window.nostr }, { path: 'navigator.nostr', getter: () => navigator?.nostr }, { path: 'window.navigator?.nostr', getter: () => window.navigator?.nostr }, { path: 'window.alby?.nostr', getter: () => window.alby?.nostr }, { path: 'window.webln?.nostr', getter: () => window.webln?.nostr }, { path: 'window.nos2x', getter: () => window.nos2x }, { path: 'window.flamingo?.nostr', getter: () => window.flamingo?.nostr }, { path: 'window.mutiny?.nostr', getter: () => window.mutiny?.nostr } ]; while (Date.now() - startTime < maxWaitMs) { console.log('🔍 AuthManager: Polling for extensions... (elapsed:', Date.now() - startTime, 'ms)'); // If our facade is currently installed and blocking, temporarily remove it let facadeRemoved = false; let originalNostr = null; if (window.nostr?.constructor?.name === 'WindowNostr') { console.log('🔍 AuthManager: Temporarily removing our facade to check for real extensions'); originalNostr = window.nostr; window.nostr = window.nostr.existingNostr || undefined; facadeRemoved = true; } try { // Check all extension locations for (const location of extensionLocations) { try { const extension = location.getter(); console.log('🔍 AuthManager: Checking', location.path, ':', !!extension, extension?.constructor?.name); if (this._isValidExtensionForRestore(extension, authState)) { console.log('🔍 AuthManager: ✅ Found matching extension at', location.path); // Restore facade if we removed it if (facadeRemoved && originalNostr) { console.log('🔍 AuthManager: Restoring facade after finding extension'); window.nostr = originalNostr; } return extension; } } catch (error) { console.log('🔍 AuthManager: Error checking', location.path, ':', error.message); } } // Restore facade if we removed it and haven't found an extension yet if (facadeRemoved && originalNostr) { window.nostr = originalNostr; facadeRemoved = false; } } catch (error) { console.error('🔍 AuthManager: Error during extension polling:', error); // Restore facade if we removed it if (facadeRemoved && originalNostr) { window.nostr = originalNostr; } } // Wait before next poll await new Promise(resolve => setTimeout(resolve, pollInterval)); } console.log('🔍 AuthManager: ❌ Extension waiting timeout after', maxWaitMs, 'ms'); return null; } // Check if an extension is valid for restoration _isValidExtensionForRestore(extension, authState) { if (!extension || typeof extension !== 'object') { return false; } // Must have required Nostr methods if (typeof extension.getPublicKey !== 'function' || typeof extension.signEvent !== 'function') { return false; } // Must not be our own classes const constructorName = extension.constructor?.name; if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') { return false; } // Must not be NostrTools if (extension === window.NostrTools) { return false; } // If we have stored verification data, check constructor match const verification = authState.extensionVerification; if (verification && verification.constructor) { if (constructorName !== verification.constructor) { console.log('🔍 AuthManager: Constructor mismatch -', 'expected:', verification.constructor, 'got:', constructorName); return false; } } console.log('🔍 AuthManager: ✅ Extension validation passed for:', constructorName); return true; } 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)); const encryptedData = CryptoUtils.base64ToArrayBuffer(authState.encrypted.data); const iv = CryptoUtils.base64ToArrayBuffer(authState.encrypted.iv); const secret = await CryptoUtils.decrypt(encryptedData, key, new Uint8Array(iv)); 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); return null; } } async _restoreNip46Auth(authState) { if (!authState.nip46) { 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'); return { method: 'nip46', pubkey: authState.pubkey, requiresReconnection: true, connectionData: authState.nip46 }; } async _restoreReadonlyAuth(authState) { console.log('AuthManager: Read-only auth restored successfully'); return { method: 'readonly', pubkey: authState.pubkey }; } // Clear stored authentication state clearAuthState() { this.storage.removeItem(this.storageKey); sessionStorage.removeItem('nostr_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(''); } // Check if we have valid stored auth hasStoredAuth() { const stored = this.storage.getItem(this.storageKey); return !!stored; } // Get current auth method without full restoration getStoredAuthMethod() { try { const stored = this.storage.getItem(this.storageKey); if (!stored) return null; const authState = JSON.parse(stored); return authState.method; } catch { return null; } } } // NIP-07 compliant window.nostr provider class WindowNostr { constructor(nostrLite, existingNostr = null) { this.nostrLite = nostrLite; this.authState = null; this.existingNostr = existingNostr; this.authenticatedExtension = null; this.authManager = new AuthManager({ isolateSession: nostrLite.options?.isolateSession }); this._setupEventListeners(); } _setupEventListeners() { // Listen for authentication events to store auth state if (typeof window !== 'undefined') { window.addEventListener('nlMethodSelected', async (event) => { 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); } // Use global setAuthState function for unified persistence try { setAuthState(event.detail, { isolateSession: this.nostrLite.options?.isolateSession }); console.log('WindowNostr: Auth state saved via global setAuthState'); } catch (error) { console.error('WindowNostr: Failed to save auth state via global setAuthState:', 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', () => { 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'); } }); } } // 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()'); } switch (this.authState.method) { case 'extension': // Use the captured authenticated extension, not current window.nostr const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.getPublicKey(); case 'local': case 'nip46': return this.authState.pubkey; case 'readonly': throw new Error('Read-only mode - cannot get public key'); default: throw new Error(`Unsupported auth method: ${this.authState.method}`); } } async signEvent(event) { if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } if (this.authState.method === 'readonly') { throw new Error('Read-only mode - cannot sign events'); } 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); case 'local': { // Use nostr-tools to sign with local secret key const { nip19, finalizeEvent } = window.NostrTools; let secretKey; if (this.authState.secret.startsWith('nsec')) { const decoded = nip19.decode(this.authState.secret); secretKey = decoded.data; } else { // Convert hex to Uint8Array secretKey = this._hexToUint8Array(this.authState.secret); } return finalizeEvent(event, secretKey); } case 'nip46': { // Use BunkerSigner for NIP-46 if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } return await this.authState.signer.bunkerSigner.signEvent(event); } default: 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']; } get nip04() { return { encrypt: async (pubkey, plaintext) => { if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } if (this.authState.method === 'readonly') { throw new Error('Read-only mode - cannot encrypt'); } switch (this.authState.method) { case 'extension': { const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip04.encrypt(pubkey, plaintext); } case 'local': { const { nip04, nip19 } = window.NostrTools; let secretKey; if (this.authState.secret.startsWith('nsec')) { const decoded = nip19.decode(this.authState.secret); secretKey = decoded.data; } else { secretKey = this._hexToUint8Array(this.authState.secret); } return await nip04.encrypt(secretKey, pubkey, plaintext); } case 'nip46': { if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } return await this.authState.signer.bunkerSigner.nip04Encrypt(pubkey, plaintext); } default: throw new Error(`Unsupported auth method: ${this.authState.method}`); } }, decrypt: async (pubkey, ciphertext) => { if (!this.authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } if (this.authState.method === 'readonly') { throw new Error('Read-only mode - cannot decrypt'); } switch (this.authState.method) { case 'extension': { const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip04.decrypt(pubkey, ciphertext); } case 'local': { const { nip04, nip19 } = window.NostrTools; let secretKey; if (this.authState.secret.startsWith('nsec')) { const decoded = nip19.decode(this.authState.secret); secretKey = decoded.data; } else { secretKey = this._hexToUint8Array(this.authState.secret); } return await nip04.decrypt(secretKey, pubkey, ciphertext); } case 'nip46': { if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } return await this.authState.signer.bunkerSigner.nip04Decrypt(pubkey, ciphertext); } default: throw new Error(`Unsupported auth method: ${this.authState.method}`); } } }; } get nip44() { return { encrypt: async (pubkey, plaintext) => { const authState = getAuthState(); if (!authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } if (authState.method === 'readonly') { throw new Error('Read-only mode - cannot encrypt'); } switch (authState.method) { case 'extension': { const ext = this.authenticatedExtension || authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip44.encrypt(pubkey, plaintext); } case 'local': { const { nip44, nip19 } = window.NostrTools; let secretKey; if (authState.secret.startsWith('nsec')) { const decoded = nip19.decode(authState.secret); secretKey = decoded.data; } else { secretKey = this._hexToUint8Array(authState.secret); } return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { if (!authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } return await authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext); } default: throw new Error('Unsupported auth method: ' + authState.method); } }, decrypt: async (pubkey, ciphertext) => { const authState = getAuthState(); if (!authState) { throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); } if (authState.method === 'readonly') { throw new Error('Read-only mode - cannot decrypt'); } switch (authState.method) { case 'extension': { const ext = this.authenticatedExtension || authState.extension || this.existingNostr; if (!ext) throw new Error('Extension not available'); return await ext.nip44.decrypt(pubkey, ciphertext); } case 'local': { const { nip44, nip19 } = window.NostrTools; let secretKey; if (authState.secret.startsWith('nsec')) { const decoded = nip19.decode(authState.secret); secretKey = decoded.data; } else { secretKey = this._hexToUint8Array(authState.secret); } return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { if (!authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } return await authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext); } default: throw new Error('Unsupported auth method: ' + authState.method); } } }; } _hexToUint8Array(hex) { if (hex.length % 2 !== 0) { throw new Error('Invalid hex string length'); } const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(hex.substr(i * 2, 2), 16); } return bytes; } } // ====================================== // 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(); // Export main API window.NOSTR_LOGIN_LITE = { init: (options) => nostrLite.init(options), launch: (startScreen) => nostrLite.launch(startScreen), logout: () => nostrLite.logout(), // Embedded modal method embed: (container, options) => nostrLite.embed(container, options), // CSS-only theme management API switchTheme: (themeName) => nostrLite.switchTheme(themeName), getCurrentTheme: () => nostrLite.getCurrentTheme(), getAvailableThemes: () => nostrLite.getAvailableThemes(), // Floating tab management API showFloatingTab: () => nostrLite.showFloatingTab(), hideFloatingTab: () => nostrLite.hideFloatingTab(), toggleFloatingTab: () => nostrLite.toggleFloatingTab(), updateFloatingTab: (options) => nostrLite.updateFloatingTab(options), getFloatingTabState: () => nostrLite.getFloatingTabState(), // GLOBAL AUTHENTICATION STATE API - Single Source of Truth getAuthState: getAuthState, setAuthState: setAuthState, clearAuthState: clearAuthState, // Expose for debugging _extensionBridge: nostrLite.extensionBridge, _instance: nostrLite }; 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'); } else { // Node.js environment module.exports = { NostrLite }; }