/** * šŸ—ļø NOSTR_LOGIN_LITE Build Script * * āš ļø IMPORTANT: This file contains the source code for the NOSTR_LOGIN_LITE library! * āš ļø DO NOT edit lite/nostr-lite.js directly - it's auto-generated by this script! * āš ļø To modify the library, edit this file (build.js) and run: node build.js * * This script builds the two-file architecture: * 1. nostr.bundle.js (official nostr-tools bundle - static file) * 2. nostr-lite.js (NOSTR_LOGIN_LITE library - built by this script) * * Features included: * - CSS-Only Theme System (no JSON duplication) * - Modal UI Component * - FloatingTab Component * - Extension Bridge * - Window.nostr facade * - Main NostrLite class with all functionality */ const fs = require('fs'); const path = require('path'); function createNostrLoginLiteBundle() { // console.log('šŸ”§ Creating NOSTR_LOGIN_LITE bundle for two-file architecture...'); const outputPath = path.join(__dirname, 'nostr-lite.js'); // Remove old bundle try { if (fs.existsSync(outputPath)) { fs.unlinkSync(outputPath); } } catch (e) { console.log('No old bundle to remove'); } // Start with the bundle header let bundle = `/** * 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: ${new Date().toISOString()} */ // 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 // ====================================== `; // Embed CSS themes // console.log('šŸŽØ Adding CSS-Only Theme System...'); const defaultThemeCssPath = path.join(__dirname, '../themes/default/theme.css'); const darkThemeCssPath = path.join(__dirname, '../themes/dark/theme.css'); if (fs.existsSync(defaultThemeCssPath) && fs.existsSync(darkThemeCssPath)) { const defaultThemeCss = fs.readFileSync(defaultThemeCssPath, 'utf8') .replace(/\\/g, '\\\\') .replace(/`/g, '\\`') .replace(/\${/g, '\\${'); const darkThemeCss = fs.readFileSync(darkThemeCssPath, 'utf8') .replace(/\\/g, '\\\\') .replace(/`/g, '\\`') .replace(/\${/g, '\\${'); bundle += `// ======================================\n`; bundle += `// CSS-Only Theme System\n`; bundle += `// ======================================\n\n`; bundle += `const THEME_CSS = {\n`; bundle += ` 'default': \`${defaultThemeCss}\`,\n`; bundle += ` 'dark': \`${darkThemeCss}\`\n`; bundle += `};\n\n`; bundle += `// Theme management functions\n`; bundle += `function injectThemeCSS(themeName = 'default') {\n`; bundle += ` if (typeof document !== 'undefined') {\n`; bundle += ` // Remove existing theme CSS\n`; bundle += ` const existingStyle = document.getElementById('nl-theme-css');\n`; bundle += ` if (existingStyle) {\n`; bundle += ` existingStyle.remove();\n`; bundle += ` }\n`; bundle += ` \n`; bundle += ` // Inject selected theme CSS\n`; bundle += ` const themeCss = THEME_CSS[themeName] || THEME_CSS['default'];\n`; bundle += ` const style = document.createElement('style');\n`; bundle += ` style.id = 'nl-theme-css';\n`; bundle += ` style.textContent = themeCss;\n`; bundle += ` document.head.appendChild(style);\n`; bundle += ` console.log('NOSTR_LOGIN_LITE: ' + themeName + ' theme CSS injected');\n`; bundle += ` }\n`; bundle += `}\n\n`; // Auto-inject default theme on load bundle += `// Auto-inject default theme when DOM is ready\n`; bundle += `if (typeof document !== 'undefined') {\n`; bundle += ` if (document.readyState === 'loading') {\n`; bundle += ` document.addEventListener('DOMContentLoaded', () => injectThemeCSS('default'));\n`; bundle += ` } else {\n`; bundle += ` injectThemeCSS('default');\n`; bundle += ` }\n`; bundle += `}\n\n`; } // Add Modal UI const modalPath = path.join(__dirname, 'ui/modal.js'); if (fs.existsSync(modalPath)) { // console.log('šŸ“„ Adding Modal UI...'); let modalContent = fs.readFileSync(modalPath, 'utf8'); // Read version from VERSION file for bottom-right display const versionPath = path.join(__dirname, 'VERSION'); let versionString = ''; if (fs.existsSync(versionPath)) { try { const version = fs.readFileSync(versionPath, 'utf8').trim(); versionString = 'v' + version; // console.log('šŸ”¢ Using version: ' + version); } catch (error) { console.warn('āš ļø Could not read VERSION file, no version will be displayed'); } } else { // console.log('šŸ“‹ No VERSION file found, no version will be displayed'); } // Keep modal title as just "Nostr Login" (no version injection) // Add version element in bottom-right corner if version exists if (versionString) { // Find the modalContent.appendChild(this.modalBody); line and insert version element before it modalContent = modalContent.replace( /modalContent\.appendChild\(this\.modalBody\);/, `// Add version element in bottom-right corner aligned with modal body const versionElement = document.createElement('div'); versionElement.textContent = '${versionString}'; versionElement.style.cssText = \` position: absolute; bottom: 8px; right: 24px; font-size: 14px; color: #666666; font-family: var(--nl-font-family, 'Courier New', monospace); pointer-events: none; z-index: 1; \`; modalContent.appendChild(versionElement); modalContent.appendChild(this.modalBody);` ); } // Skip header comments let lines = modalContent.split('\n'); let contentStartIndex = 0; for (let i = 0; i < Math.min(15, lines.length); i++) { const line = lines[i].trim(); if (line.startsWith('/**') || line.startsWith('*') || line.startsWith('/*') || line.startsWith('//')) { contentStartIndex = i + 1; } else if (line && !line.startsWith('*') && !line.startsWith('//')) { break; } } if (contentStartIndex > 0) { lines = lines.slice(contentStartIndex); } bundle += `// ======================================\n`; bundle += `// Modal UI Component\n`; bundle += `// ======================================\n\n`; bundle += lines.join('\n'); bundle += '\n\n'; } else { console.warn('āš ļø Modal UI not found: ui/modal.js'); } // Add main library code // console.log('šŸ“„ Adding Main Library...'); bundle += ` // ====================================== // 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'); // CRITICAL FIX: Immediately attempt to restore auth state after facade installation if (this.facadeInstalled && window.nostr?.restoreAuthState) { console.log('šŸ” NOSTR_LOGIN_LITE: šŸ”„ IMMEDIATELY attempting auth restoration after facade installation'); try { const restoredAuth = await window.nostr.restoreAuthState(); if (restoredAuth) { console.log('šŸ” NOSTR_LOGIN_LITE: āœ… Auth state restored immediately during facade setup!'); console.log('šŸ” NOSTR_LOGIN_LITE: Method:', restoredAuth.method); console.log('šŸ” NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey); // Update facade's authState immediately window.nostr.authState = restoredAuth; } else { console.log('šŸ” NOSTR_LOGIN_LITE: āŒ No auth state to restore during facade setup'); } } catch (error) { console.error('šŸ” NOSTR_LOGIN_LITE: āŒ Error restoring auth during facade setup:', error); } } } } _installFacade(existingNostr = null, forceInstall = false) { if (typeof window !== 'undefined' && (!this.facadeInstalled || forceInstall)) { console.log('šŸ” NOSTR_LOGIN_LITE: === _installFacade CALLED ==='); console.log('šŸ” NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr); console.log('šŸ” NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr); console.log('šŸ” NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name); console.log('šŸ” NOSTR_LOGIN_LITE: forceInstall flag:', forceInstall); const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession }); window.nostr = facade; 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); } else if (typeof window !== 'undefined') { console.log('šŸ” NOSTR_LOGIN_LITE: _installFacade skipped - facadeInstalled:', this.facadeInstalled, 'forceInstall:', forceInstall); } } // 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); // CRITICAL FIX: Activate facade resilience system for non-extension methods // Extensions like nos2x can override our facade after page refresh if (restoredAuth.method === 'local' || restoredAuth.method === 'nip46') { console.log('šŸ” NOSTR_LOGIN_LITE: šŸ›”ļø Activating facade resilience system for page refresh'); this._activateResilienceProtection(restoredAuth.method); } // Handle NIP-46 reconnection requirement if (restoredAuth.requiresReconnection) { console.log('šŸ” NOSTR_LOGIN_LITE: NIP-46 connection requires user reconnection'); 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; } } // Activate facade resilience protection against extension overrides _activateResilienceProtection(method) { console.log('šŸ›”ļø NOSTR_LOGIN_LITE: === ACTIVATING RESILIENCE PROTECTION ==='); console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Protecting facade for method:', method); // Store the current extension if any (for potential restoration later) const preservedExtension = this.preservedExtension || ((window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null); // DELAYED FACADE RESILIENCE - Reinstall after extension override attempts const forceReinstallFacade = () => { console.log('šŸ›”ļø NOSTR_LOGIN_LITE: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name); // If facade was overridden by extension, reinstall it if (window.nostr?.constructor?.name !== 'WindowNostr') { console.log('šŸ›”ļø NOSTR_LOGIN_LITE: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method); this._installFacade(preservedExtension, true); console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name); // Schedule another check in case of persistent extension override setTimeout(() => { if (window.nostr?.constructor?.name !== 'WindowNostr') { console.log('šŸ›”ļø NOSTR_LOGIN_LITE: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method); this._installFacade(preservedExtension, true); } }, 1000); } else { console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Facade persistence verified - no override detected'); } }; // Schedule resilience checks at multiple intervals (same as Modal) setTimeout(forceReinstallFacade, 100); // Quick check setTimeout(forceReinstallFacade, 500); // Main check setTimeout(forceReinstallFacade, 1500); // Final check console.log('šŸ›”ļø NOSTR_LOGIN_LITE: Resilience protection scheduled for method:', method); } // Extension-specific authentication restoration async _attemptExtensionRestore() { try { 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; } } // ====================================== // Simplified Authentication Manager (Unified Plaintext Storage) // ====================================== // Simple authentication state manager - plaintext storage for maximum usability 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'); } console.warn('šŸ” SECURITY: Private keys stored unencrypted in browser storage'); console.warn('šŸ” For production apps, implement your own secure storage'); } // Save authentication state using unified plaintext approach async saveAuthState(authData) { try { console.log('šŸ” AuthManager: Saving auth state with plaintext storage'); console.warn('šŸ” SECURITY: Private key will be stored unencrypted for maximum usability'); const authState = { method: authData.method, timestamp: Date.now(), 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' }; console.log('šŸ” AuthManager: Extension method - storing verification data only'); break; case 'local': // UNIFIED PLAINTEXT: Store secret key directly for maximum compatibility if (authData.secret) { authState.secret = authData.secret; console.log('šŸ” AuthManager: Local method - storing secret key in plaintext'); console.warn('šŸ” SECURITY: Secret key stored unencrypted for developer convenience'); } break; 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 }; console.log('šŸ” AuthManager: NIP-46 method - storing connection parameters'); } break; case 'readonly': // Read-only mode has no secrets to store console.log('šŸ” AuthManager: Read-only method - storing basic auth state'); break; default: throw new Error('Unknown auth method: ' + authData.method); } this.storage.setItem(this.storageKey, JSON.stringify(authState)); this.currentAuthState = authState; console.log('šŸ” AuthManager: Auth state saved successfully 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: Storage 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) { console.log('šŸ” AuthManager: === _restoreLocalAuth (Unified Plaintext) ==='); // Check for legacy encrypted format first if (authState.encrypted) { console.log('šŸ” AuthManager: Detected LEGACY encrypted format - migrating to plaintext'); console.warn('šŸ” SECURITY: Converting from encrypted to plaintext storage for compatibility'); // Try to decrypt legacy format const sessionPassword = sessionStorage.getItem('nostr_session_key'); if (!sessionPassword) { console.log('šŸ” AuthManager: Legacy session password not found - user must re-login'); return null; } try { console.warn('šŸ” AuthManager: Legacy encryption system no longer supported - user must re-login'); this.clearAuthState(); // Clear legacy format return null; } catch (error) { console.error('šŸ” AuthManager: Legacy decryption failed:', error); this.clearAuthState(); // Clear corrupted legacy format return null; } } // NEW UNIFIED PLAINTEXT FORMAT if (!authState.secret) { console.log('šŸ” AuthManager: No secret found in plaintext format'); return null; } console.log('šŸ” AuthManager: āœ… Local auth restored from plaintext storage'); console.warn('šŸ” SECURITY: Secret key was stored unencrypted'); return { method: 'local', pubkey: authState.pubkey, secret: authState.secret }; } async _restoreNip46Auth(authState) { if (!authState.nip46) { console.log('šŸ” AuthManager: No NIP-46 data found'); 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'); // Clear legacy session key this.currentAuthState = null; console.log('šŸ” AuthManager: Auth state cleared from unified storage'); } // 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; } } } // ====================================== // Global Authentication Functions (Single Source of Truth) // ====================================== // Global authentication state (single source of truth) let globalAuthState = null; let globalAuthManager = null; // Initialize global auth manager (lazy initialization) function getGlobalAuthManager() { if (!globalAuthManager) { // Default to localStorage for persistence across browser sessions globalAuthManager = new AuthManager({ isolateSession: false }); } return globalAuthManager; } // **UNIFIED GLOBAL FUNCTION**: Set authentication state (works for all methods) function setAuthState(authData, options = {}) { try { console.log('🌐 setAuthState: Setting global auth state for method:', authData.method); console.warn('šŸ” SECURITY: Using unified plaintext storage for maximum compatibility'); // Store in memory globalAuthState = authData; // Store in browser storage using AuthManager const authManager = new AuthManager(options); authManager.saveAuthState(authData); console.log('🌐 setAuthState: Auth state saved successfully'); } catch (error) { console.error('🌐 setAuthState: Failed to save auth state:', error); throw error; } } // **UNIFIED GLOBAL FUNCTION**: Get authentication state (single source of truth) function getAuthState() { try { // Always query from storage as the authoritative source const authManager = getGlobalAuthManager(); const storageKey = 'nostr_login_lite_auth'; // Check both session and local storage for compatibility let stored = null; if (sessionStorage.getItem(storageKey)) { stored = sessionStorage.getItem(storageKey); } else if (localStorage.getItem(storageKey)) { stored = localStorage.getItem(storageKey); } if (!stored) { console.log('🌐 getAuthState: No auth state found in storage'); globalAuthState = null; return null; } const authState = JSON.parse(stored); console.log('🌐 getAuthState: Retrieved auth state:', authState.method); // Update in-memory cache globalAuthState = authState; return authState; } catch (error) { console.error('🌐 getAuthState: Failed to get auth state:', error); globalAuthState = null; return null; } } // **UNIFIED GLOBAL FUNCTION**: Clear authentication state (works for all methods) function clearAuthState() { try { console.log('🌐 clearAuthState: Clearing global auth state'); // Clear in-memory state globalAuthState = null; // Clear from both storage types for thorough cleanup const storageKey = 'nostr_login_lite_auth'; localStorage.removeItem(storageKey); sessionStorage.removeItem(storageKey); sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key console.log('🌐 clearAuthState: Auth state cleared from all storage locations'); } catch (error) { console.error('🌐 clearAuthState: Failed to clear auth state:', error); } } // NIP-07 compliant window.nostr provider class WindowNostr { constructor(nostrLite, existingNostr = null, options = {}) { this.nostrLite = nostrLite; this.authState = null; this.existingNostr = existingNostr; this.authenticatedExtension = null; this.options = options; this._setupEventListeners(); } // Restore authentication state on page load async restoreAuthState() { console.log('šŸ” WindowNostr: === restoreAuthState ==='); try { // Use simplified AuthManager for consistent restore logic const authManager = new AuthManager(this.options); const restoredAuth = await authManager.restoreAuthState(); if (restoredAuth) { console.log('šŸ” WindowNostr: āœ… Auth state restored:', restoredAuth.method); this.authState = restoredAuth; // Update global state globalAuthState = restoredAuth; // Dispatch restoration event if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('nlAuthRestored', { detail: restoredAuth })); } return restoredAuth; } else { console.log('šŸ” WindowNostr: āŒ No auth state to restore'); return null; } } catch (error) { console.error('šŸ” WindowNostr: Auth restoration failed:', error); return null; } } _setupEventListeners() { // Listen for authentication events to store auth state if (typeof window !== 'undefined') { window.addEventListener('nlMethodSelected', async (event) => { console.log('šŸ” WindowNostr: nlMethodSelected event received:', event.detail); this.authState = event.detail; // If extension method, capture the specific extension the user chose if (event.detail.method === 'extension') { this.authenticatedExtension = event.detail.extension; console.log('šŸ” WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); } // Use unified global setAuthState function for all methods try { setAuthState(event.detail, this.options); console.log('šŸ” WindowNostr: Auth state saved via unified setAuthState'); } catch (error) { console.error('šŸ” WindowNostr: Failed to save auth state:', error); } }); window.addEventListener('nlLogout', () => { console.log('šŸ” WindowNostr: nlLogout event received'); this.authState = null; this.authenticatedExtension = null; // Clear from unified storage clearAuthState(); console.log('šŸ” WindowNostr: Auth state cleared via unified clearAuthState'); }); } } 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 const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; 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 configured relays from nostr-lite options return this.nostrLite.options?.relays || ['wss://relay.damus.io']; } 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) => { 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.nip44.encrypt(pubkey, plaintext); } case 'local': { const { nip44, 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 nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } return await this.authState.signer.bunkerSigner.nip44Encrypt(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.nip44.decrypt(pubkey, ciphertext); } case 'local': { const { nip44, 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 nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey)); } case 'nip46': { if (!this.authState.signer?.bunkerSigner) { throw new Error('NIP-46 signer not available'); } return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext); } default: throw new Error('Unsupported auth method: ' + this.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; } } // 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 management (single source of truth) setAuthState: setAuthState, getAuthState: getAuthState, 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'); console.warn('šŸ” SECURITY: Unified plaintext storage enabled for maximum developer usability'); } else { // Node.js environment module.exports = { NostrLite }; } `; // Write the complete bundle fs.writeFileSync(outputPath, bundle, 'utf8'); const sizeKB = (bundle.length / 1024).toFixed(2); console.log('\nāœ… nostr-lite.js bundle created: ' + outputPath); console.log('šŸ“ Bundle size: ' + sizeKB + ' KB'); console.log('šŸ“„ Total lines: ' + bundle.split('\n').length); // Check what's included const hasModal = bundle.includes('class Modal'); const hasNostrLite = bundle.includes('NOSTR_LOGIN_LITE'); const hasThemeCss = bundle.includes('THEME_CSS'); // console.log('\nšŸ“‹ Bundle contents:'); // console.log(' Modal UI: ' + (hasModal ? 'āœ… Included' : 'āŒ Missing')); // console.log(' NOSTR_LOGIN_LITE: ' + (hasNostrLite ? 'āœ… Included' : 'āŒ Missing')); // console.log(' CSS-Only Themes: ' + (hasThemeCss ? 'āœ… Included' : 'āŒ Missing')); // console.log(' Extension Bridge: āœ… Included'); // console.log(' Window.nostr facade: āœ… Included'); // console.log('\nšŸ“‹ Two-file architecture:'); // console.log(' 1. nostr.bundle.js (official nostr-tools - 220KB)'); // console.log(' 2. nostr-lite.js (NOSTR_LOGIN_LITE with CSS-only themes - ' + sizeKB + 'KB)'); return bundle; } // Run if called directly if (typeof require !== 'undefined' && require.main === module) { createNostrLoginLiteBundle(); } module.exports = { createNostrLoginLiteBundle };