/** * NOSTR_LOGIN_LITE * A minimal, dependency-light replacement for the current auth/UI stack * Preserves all login methods and window.nostr surface */ // Import NIP-46 client if (typeof NIP46Client === 'undefined') { // Load NIP46Client if not already available (for non-bundled version) const script = document.createElement('script'); script.src = './core/nip46-client.js'; document.head.appendChild(script); } // Global state const LiteState = { initialized: false, windowNostr: null, options: null, auth: null, modal: null, bus: null, pool: null, nip44Codec: null, extensionBridge: null, nip46Client: null }; // Dependencies verification class Deps { static ensureNostrToolsLoaded() { if (typeof window === 'undefined') { throw new Error('NOSTR_LOGIN_LITE must run in browser environment'); } if (!window.NostrTools) { throw new Error( 'window.NostrTools is required but not loaded. ' + 'Please include: ' ); } // Verify required APIs const required = ['SimplePool', 'getPublicKey', 'finalizeEvent', 'nip04']; for (const api of required) { if (!window.NostrTools[api]) { throw new Error(`window.NostrTools.${api} is required but missing`); } } // Check for key generation function (might be generateSecretKey or generatePrivateKey) if (!window.NostrTools.generateSecretKey && !window.NostrTools.generatePrivateKey) { throw new Error('window.NostrTools must have either generateSecretKey or generatePrivateKey'); } return true; } } // Event Bus for internal communication class Bus { constructor() { this.handlers = {}; } on(event, handler) { if (!this.handlers[event]) { this.handlers[event] = []; } this.handlers[event].push(handler); } off(event, handler) { if (!this.handlers[event]) return; this.handlers[event] = this.handlers[event].filter(h => h !== handler); } emit(event, payload) { if (!this.handlers[event]) return; this.handlers[event].forEach(handler => { try { handler(payload); } catch (e) { console.error(`Error in event handler for ${event}:`, e); } }); } } // Storage helpers class Store { static addAccount(info) { const accounts = this.getAccounts(); // Remove existing account with same pubkey if present const filtered = accounts.filter(acc => acc.pubkey !== info.pubkey); filtered.push(info); localStorage.setItem('nl_accounts', JSON.stringify(filtered)); } static removeCurrentAccount() { const current = this.getCurrent(); if (current && current.pubkey) { const accounts = this.getAccounts(); const filtered = accounts.filter(acc => acc.pubkey !== current.pubkey); localStorage.setItem('nl_accounts', JSON.stringify(filtered)); localStorage.removeItem('nl_current'); } } static getCurrent() { try { const stored = localStorage.getItem('nl_current'); return stored ? JSON.parse(stored) : null; } catch (e) { console.error('Error parsing current account:', e); return null; } } static setCurrent(info) { localStorage.setItem('nl_current', JSON.stringify(info)); } static getAccounts() { try { const stored = localStorage.getItem('nl_accounts'); return stored ? JSON.parse(stored) : []; } catch (e) { console.error('Error parsing accounts:', e); return []; } } static getRecents() { // Return last 5 used accounts in reverse chronological order const accounts = this.getAccounts().slice(-5).reverse(); return accounts; } static setItem(key, value) { localStorage.setItem(`nl-${key}`, value); } static getItem(key) { return localStorage.getItem(`nl-${key}`); } static async getIcon() { // Simple default icon - could be extended to fetch from profile return '🔑'; } } // Relay configuration helpers class Relays { static getDefaultRelays(options) { if (options?.relays) { return this.normalize(options.relays); } // Default relays for fallbacks return [ 'wss://relay.damus.io', 'wss://relay.snort.social', 'wss://nos.lol' ]; } static normalize(relays) { return relays.map(relay => { // Ensure wss:// prefix if (relay.startsWith('ws://')) { return relay.replace('ws://', 'wss://'); } else if (!relay.startsWith('wss://')) { return `wss://${relay}`; } return relay; }).filter(relay => { // Remove duplicates and validate URLs try { new URL(relay); return true; } catch { return false; } }).filter((relay, index, self) => self.indexOf(relay) === index); // dedupe } } // Minimal NIP-44 codec fallback class Nip44 { constructor() { this.Nip44 = null; // Initialize with existing codec if available this.nip44Available = window.NostrTools?.nip44; } static encrypt(ourSk, theirPk, plaintext) { if (window.NostrTools?.nip44?.encrypt) { return window.NostrTools.nip44.encrypt(ourSk, theirPk, plaintext); } throw new Error('NIP-44 encryption not available. Please use nostr-tools@>=2.x or provide codec implementation.'); } static decrypt(ourSk, theirPk, ciphertext) { if (window.NostrTools?.nip44?.decrypt) { return window.NostrTools.nip44.decrypt(ourSk, theirPk, ciphertext); } throw new Error('NIP-44 decryption not available. Please use nostr-tools@>=2.x or provide codec implementation.'); } } // LocalSigner wrapping window.NostrTools class LocalSigner { constructor(sk) { this.sk = sk; // Generate pubkey from secret key this.pk = this._getPubKey(); } _getPubKey() { const seckey = this.sk.startsWith('nsec') ? window.NostrTools.nip19.decode(this.sk).data : this.sk; return window.NostrTools.getPublicKey(seckey); } pubkey() { return this.pk; } async sign(event) { // Prepare event for signing const ev = { ...event }; ev.pubkey = this.pk; // Generate event ID and sign const signedEvent = await window.NostrTools.finalizeEvent(ev, this.sk); return signedEvent; } async encrypt04(pubkey, plaintext) { return await window.NostrTools.nip04.encrypt(this.sk, pubkey, plaintext); } async decrypt04(pubkey, ciphertext) { return await window.NostrTools.nip04.decrypt(this.sk, pubkey, ciphertext); } async encrypt44(pubkey, plaintext) { return Nip44.encrypt(this.sk, pubkey, plaintext); } async decrypt44(pubkey, ciphertext) { return Nip44.decrypt(this.sk, pubkey, ciphertext); } } // ExtensionBridge for detecting and managing browser extensions class ExtensionBridge { constructor() { this.checking = false; this.checkInterval = null; this.originalNostr = null; this.foundExtensions = new Map(); // Store multiple extensions by location this.primaryExtension = null; // The currently selected extension } startChecking(nostrLite) { if (this.checking) return; this.checking = true; const check = () => { this.detectAllExtensions(nostrLite); }; // Check immediately check(); // Then check every 200ms for 30 seconds this.checkInterval = setInterval(check, 200); // Stop checking after 30 seconds setTimeout(() => { clearInterval(this.checkInterval); this.checkInterval = null; }, 30000); } detectAllExtensions(nostrLite) { // Extension locations to check (in priority order) const locations = [ { path: 'window.navigator?.nostr', name: 'navigator.nostr', getter: () => window.navigator?.nostr }, { path: 'window.webln?.nostr', name: 'webln.nostr', getter: () => window.webln?.nostr }, { path: 'window.alby?.nostr', name: 'alby.nostr', getter: () => window.alby?.nostr }, { path: 'window.nos2x', name: 'nos2x', getter: () => window.nos2x }, { path: 'window.flamingo?.nostr', name: 'flamingo.nostr', getter: () => window.flamingo?.nostr }, { path: 'window.mutiny?.nostr', name: 'mutiny.nostr', getter: () => window.mutiny?.nostr }, { path: 'window.nostrich?.nostr', name: 'nostrich.nostr', getter: () => window.nostrich?.nostr }, { path: 'window.getAlby?.nostr', name: 'getAlby.nostr', getter: () => window.getAlby?.nostr } ]; let foundNew = false; // Check each location for (const location of locations) { try { const obj = location.getter(); if (obj && this.isRealExtension(obj, nostrLite)) { if (!this.foundExtensions.has(location.name)) { this.foundExtensions.set(location.name, { name: location.name, path: location.path, extension: obj, constructor: obj.constructor?.name || 'Unknown' }); console.log(`Real Nostr extension detected: ${location.name} (${obj.constructor?.name})`); foundNew = true; } } } catch (e) { // Location doesn't exist or can't be accessed } } // Also check window.nostr but be extra careful to avoid our library if (window.nostr && this.isRealExtension(window.nostr, nostrLite)) { // Make sure we haven't already detected this extension via another path const existingExtension = Array.from(this.foundExtensions.values()).find( ext => ext.extension === window.nostr ); if (!existingExtension && !this.foundExtensions.has('window.nostr')) { this.foundExtensions.set('window.nostr', { name: 'window.nostr', path: 'window.nostr', extension: window.nostr, constructor: window.nostr.constructor?.name || 'Unknown' }); console.log(`Real Nostr extension detected at window.nostr: ${window.nostr.constructor?.name}`); foundNew = true; } } // Set primary extension if we don't have one and found extensions if (!this.primaryExtension && this.foundExtensions.size > 0) { // Prefer navigator.nostr if available, otherwise use first found this.primaryExtension = this.foundExtensions.get('navigator.nostr') || Array.from(this.foundExtensions.values())[0]; // Cache the extension and reassign window.nostr to our lite version this.originalNostr = this.primaryExtension.extension; if (window.nostr !== nostrLite) { window.nostr = nostrLite; } console.log(`Primary extension set: ${this.primaryExtension.name}`); // If currently authenticated, reconcile state if (LiteState.auth?.signer?.method === 'extension') { this.reconcileExtension(); } } } isRealExtension(obj, nostrLite) { 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 objects if (obj === nostrLite || obj === windowNostr) { return false; } // Exclude objects with our library's internal methods if (typeof obj._hexToUint8Array === 'function' || typeof obj._call === 'function') { return false; } // Exclude NostrTools library object if (obj === window.NostrTools) { return false; } // Real extensions typically have proper constructors (not plain Object) const constructorName = obj.constructor?.name; if (constructorName === 'Object' && !obj._isEnabled && !obj.enabled) { // Plain objects without extension-specific properties are likely our library return false; } return true; } getAllExtensions() { return Array.from(this.foundExtensions.values()); } getExtensionCount() { return this.foundExtensions.size; } hasExtension() { return this.foundExtensions.size > 0; } // Legacy compatibility - return primary extension get foundExtension() { return this.primaryExtension?.extension || null; } // Method to properly set primary extension setPrimaryExtension(extension, name = 'selected') { // Find the extension in our map or create new entry let extensionInfo = null; // Check if this extension is already in our map for (const [key, info] of this.foundExtensions) { if (info.extension === extension) { extensionInfo = info; break; } } // If not found, create a new entry if (!extensionInfo) { extensionInfo = { name: name, path: name, extension: extension, constructor: extension?.constructor?.name || 'Unknown' }; this.foundExtensions.set(name, extensionInfo); } this.primaryExtension = extensionInfo; console.log(`Primary extension set to: ${extensionInfo.name}`); } async setExtensionReadPubkey(expectedPubkey = null) { if (!this.primaryExtension) return false; try { // Temporarily set window.nostr to extension const temp = window.nostr; window.nostr = this.primaryExtension.extension; const pubkey = await this.primaryExtension.extension.getPublicKey(); // Restore our lite implementation window.nostr = temp; if (expectedPubkey && pubkey !== expectedPubkey) { console.warn(`Extension pubkey ${pubkey} does not match expected ${expectedPubkey}`); } return pubkey; } catch (e) { console.error('Error reading extension pubkey:', e); return null; } } trySetForPubkey(expectedPubkey) { if (!this.hasExtension()) return false; this.setExtensionReadPubkey(expectedPubkey).then(pubkey => { if (pubkey) { LiteState.bus?.emit('extensionLogin', { pubkey }); } }); return true; } setExtension() { if (!this.primaryExtension) return; window.nostr = this.primaryExtension.extension; this.setExtensionReadPubkey().then(pubkey => { if (pubkey) { LiteState.bus?.emit('extensionSet', { pubkey }); } }); } unset(nostrLite) { window.nostr = nostrLite; } reconcileExtension() { // Handle extension state changes this.setExtensionReadPubkey().then(pubkey => { if (pubkey) { // Update current account if extension is the signer const current = Store.getCurrent(); if (current && current.signer?.method === 'extension') { const info = { ...current, pubkey, signer: { method: 'extension' } }; Store.setCurrent(info); LiteState.bus?.emit('authStateUpdate', info); } } }); } } // Main API surface class NostrLite { static async init(options = {}) { // Ensure dependencies are loaded Deps.ensureNostrToolsLoaded(); // Prevent double initialization if (LiteState.initialized) { console.warn('NOSTR_LOGIN_LITE already initialized'); return; } // Initialize components LiteState.bus = new Bus(); LiteState.extensionBridge = new ExtensionBridge(); // Initialize NIP-46 client LiteState.nip46Client = new NIP46Client(); // Store options LiteState.options = { theme: 'light', darkMode: false, relays: Relays.getDefaultRelays(options), methods: { connect: true, extension: true, local: true, readonly: true, otp: true }, otp: {}, ...options }; // Start extension detection LiteState.extensionBridge.startChecking(windowNostr); // Setup auth methods this._setupAuth(); // Initialize modal UI // this._initModal(); console.log('NOSTR_LOGIN_LITE initialized with options:', LiteState.options); LiteState.initialized = true; } static _setupAuth() { // Set up event listeners for modal interactions window.addEventListener('nlMethodSelected', (event) => { this._handleMethodSelected(event.detail); }); // Set up other auth-related event listeners this._setupAuthEventListeners(); console.log('Auth system setup loaded'); } static _setupAuthEventListeners() { // Handle extension detection this.bus?.on('extensionDetected', (extension) => { console.log('Extension detected'); LiteState.extensionBridge.setPrimaryExtension(extension, 'detected'); }); // Handle auth URL from NIP-46 window.addEventListener('nlAuthUrl', (event) => { console.log('Auth URL received:', event.detail.url); // Could show URL in modal or trigger external flow }); // Handle logout events window.addEventListener('nlLogout', () => { console.log('Logout event received'); this.logout(); }); } static _handleMethodSelected(detail) { console.log('Method selected:', detail); const { method, pubkey, secret, extension } = detail; switch (method) { case 'local': if (secret && pubkey) { // Set up local key authentication const info = { pubkey, signer: { method: 'local', secret } }; Store.setCurrent(info); LiteState.bus?.emit('authStateUpdate', info); this._dispatchAuthEvent('login', info); } break; case 'extension': if (pubkey && extension) { // Store the extension object in the ExtensionBridge for future use LiteState.extensionBridge.setPrimaryExtension(extension, 'modal-selected'); LiteState.extensionBridge.originalNostr = extension; // Set up extension authentication const info = { pubkey, signer: { method: 'extension' } }; Store.setCurrent(info); LiteState.bus?.emit('authStateUpdate', info); this._dispatchAuthEvent('login', info); console.log('Extension authentication set up successfully'); } else { // Fallback to extension bridge detection LiteState.bus?.emit('authMethodSelected', { method: 'extension' }); } break; case 'readonly': // Set read-only mode const readonlyInfo = { pubkey: '', signer: { method: 'readonly' } }; Store.setCurrent(readonlyInfo); LiteState.bus?.emit('authStateUpdate', readonlyInfo); this._dispatchAuthEvent('login', readonlyInfo); break; case 'nip46': if (secret && pubkey) { // Set up NIP-46 remote signing const info = { pubkey, signer: { method: 'nip46', ...secret } }; Store.setCurrent(info); LiteState.bus?.emit('authStateUpdate', info); this._dispatchAuthEvent('login', info); } break; default: console.warn('Unhandled auth method:', method); } } static _dispatchAuthEvent(type, info) { const eventPayload = { type, info, pubkey: info?.pubkey || '', method: info?.signer?.method || '', ...info }; // Dispatch the event window.dispatchEvent(new CustomEvent('nlAuth', { detail: eventPayload })); this.bus?.emit('nlAuth', eventPayload); } static launch(startScreen) { if (!LiteState.initialized) { throw new Error('NOSTR_LOGIN_LITE not initialized. Call init() first.'); } console.log('Launch requested with screen:', startScreen); // Initialize modal if needed if (!LiteState.modal) { // Import modal lazily if (typeof Modal !== 'undefined') { LiteState.modal = Modal.init(LiteState.options); } else { console.error('Modal component not available'); return; } } // Open modal with specified screen LiteState.modal.open({ startScreen }); } static logout() { if (!LiteState.initialized) return; // Clear current account and state Store.removeCurrentAccount(); // Reset internal state LiteState.auth = null; // Emit logout event window.dispatchEvent(new CustomEvent('nlLogout')); LiteState.bus?.emit('logout'); console.log('Logged out'); } static setDarkMode(dark) { if (!LiteState.options) return; LiteState.options.darkMode = dark; Store.setItem('darkMode', dark.toString()); // Update modal theme if initialized if (LiteState.modal) { // LiteState.modal.updateTheme(); } window.dispatchEvent(new CustomEvent('nlDarkMode', { detail: { dark } })); } static setAuth(o) { if (!o || !o.type) return; console.log('setAuth called:', o); // Validate request if (!['login', 'signup', 'logout'].includes(o.type)) { throw new Error(`Invalid auth type: ${o.type}`); } if (['login', 'signup'].includes(o.type) && !['connect', 'extension', 'local', 'otp', 'readOnly'].includes(o.method)) { throw new Error(`Invalid auth method: ${o.method}`); } // Handle based on type switch (o.type) { case 'logout': this.logout(); break; default: // Delegate to auth system - will be implemented console.log('Auth delegation not yet implemented'); } } static cancelNeedAuth() { // Cancel any ongoing auth flows LiteState.bus?.emit('cancelAuth'); console.log('Auth flow cancelled'); } } // Initialize the window.nostr facade const windowNostr = { async getPublicKey() { if (!LiteState.initialized) { throw new Error('NOSTR_LOGIN_LITE not initialized'); } const current = Store.getCurrent(); if (current && current.pubkey) { return current.pubkey; } // Trigger auth flow const authPromise = new Promise((resolve, reject) => { const handleAuth = (event) => { window.removeEventListener('nlAuth', handleAuth); if (event.detail.type === 'login' && event.detail.pubkey) { resolve(event.detail.pubkey); } else { reject(new Error('Authentication cancelled')); } }; window.addEventListener('nlAuth', handleAuth); // Set timeout setTimeout(() => { window.removeEventListener('nlAuth', handleAuth); reject(new Error('Authentication timeout')); }, 300000); // 5 minutes }); // Launch auth modal NostrLite.launch('login'); return authPromise; }, async signEvent(event) { if (!LiteState.initialized) { throw new Error('NOSTR_LOGIN_LITE not initialized'); } let current = Store.getCurrent(); // If no current account, trigger auth if (!current) { await window.nostr.getPublicKey(); // This will trigger auth current = Store.getCurrent(); if (!current) { throw new Error('Authentication failed'); } } // Route to appropriate signer if (current.signer?.method === 'local' && current.signer.secret) { const signer = new LocalSigner(this._hexToUint8Array(current.signer.secret)); return await signer.sign(event); } else if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) { // Route to NIP-46 remote signer try { const bunkerSigner = current.signer.bunkerSigner; const signedEvent = await bunkerSigner.signEvent(event); return signedEvent; } catch (error) { console.error('NIP-46 signEvent failed:', error); throw new Error(`NIP-46 signing failed: ${error.message}`); } } else if (current.signer?.method === 'readonly') { throw new Error('Cannot sign events in read-only mode'); } else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) { // Route to extension const temp = window.nostr; window.nostr = LiteState.extensionBridge.foundExtension; try { const signedEvent = await window.nostr.signEvent(event); return signedEvent; } finally { window.nostr = temp; } } throw new Error('No suitable signer available for current account'); }, _hexToUint8Array(hex) { // Convert hex string to Uint8Array 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; }, nip04: { async encrypt(pubkey, plaintext) { if (!LiteState.initialized) { throw new Error('NOSTR_LOGIN_LITE not initialized'); } const current = Store.getCurrent(); if (!current) { throw new Error('No authenticated user'); } if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) { // Route to NIP-46 remote signer try { const bunkerSigner = current.signer.bunkerSigner; return await bunkerSigner.nip04Encrypt(pubkey, plaintext); } catch (error) { console.error('NIP-46 nip04 encrypt failed:', error); throw new Error(`NIP-46 encrypting failed: ${error.message}`); } } else if (current.signer?.method === 'local' && current.signer.secret) { const signer = new LocalSigner(current.signer.secret); return await signer.encrypt04(pubkey, plaintext); } else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) { const temp = window.nostr; window.nostr = LiteState.extensionBridge.foundExtension; try { return await window.nostr.nip04.encrypt(pubkey, plaintext); } finally { window.nostr = temp; } } throw new Error('No suitable signer available for NIP-04 encryption'); }, async decrypt(pubkey, ciphertext) { if (!LiteState.initialized) { throw new Error('NOSTR_LOGIN_LITE not initialized'); } const current = Store.getCurrent(); if (!current) { throw new Error('No authenticated user'); } if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) { // Route to NIP-46 remote signer try { const bunkerSigner = current.signer.bunkerSigner; return await bunkerSigner.nip04Decrypt(pubkey, ciphertext); } catch (error) { console.error('NIP-46 nip04 decrypt failed:', error); throw new Error(`NIP-46 decrypting failed: ${error.message}`); } } else if (current.signer?.method === 'local' && current.signer.secret) { const signer = new LocalSigner(current.signer.secret); return await signer.decrypt04(pubkey, ciphertext); } else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) { const temp = window.nostr; window.nostr = LiteState.extensionBridge.foundExtension; try { return await window.nostr.nip04.decrypt(pubkey, ciphertext); } finally { window.nostr = temp; } } throw new Error('No suitable signer available for NIP-04 decryption'); } }, nip44: { async encrypt(pubkey, plaintext) { if (!LiteState.initialized) { throw new Error('NOSTR_LOGIN_LITE not initialized'); } const current = Store.getCurrent(); if (!current) { throw new Error('No authenticated user'); } if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) { // Route to NIP-46 remote signer try { const bunkerSigner = current.signer.bunkerSigner; return await bunkerSigner.nip44Encrypt(pubkey, plaintext); } catch (error) { console.error('NIP-46 nip44 encrypt failed:', error); throw new Error(`NIP-46 encrypting failed: ${error.message}`); } } else if (current.signer?.method === 'local' && current.signer.secret) { const signer = new LocalSigner(current.signer.secret); return await signer.encrypt44(pubkey, plaintext); } else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) { // Use extension if it supports nip44 const temp = window.nostr; window.nostr = LiteState.extensionBridge.foundExtension; try { if (window.nostr.nip44) { return await window.nostr.nip44.encrypt(pubkey, plaintext); } else { throw new Error('Extension does not support NIP-44'); } } finally { window.nostr = temp; } } throw new Error('No suitable signer available for NIP-44 encryption'); }, async decrypt(pubkey, ciphertext) { if (!LiteState.initialized) { throw new Error('NOSTR_LOGIN_LITE not initialized'); } const current = Store.getCurrent(); if (!current) { throw new Error('No authenticated user'); } if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) { // Route to NIP-46 remote signer try { const bunkerSigner = current.signer.bunkerSigner; return await bunkerSigner.nip44Decrypt(pubkey, ciphertext); } catch (error) { console.error('NIP-46 nip44 decrypt failed:', error); throw new Error(`NIP-46 decrypting failed: ${error.message}`); } } else if (current.signer?.method === 'local' && current.signer.secret) { const signer = new LocalSigner(current.signer.secret); return await signer.decrypt44(pubkey, ciphertext); } else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) { const temp = window.nostr; window.nostr = LiteState.extensionBridge.foundExtension; try { if (window.nostr.nip44) { return await window.nostr.nip44.decrypt(pubkey, ciphertext); } else { throw new Error('Extension does not support NIP-44'); } } finally { window.nostr = temp; } } throw new Error('No suitable signer available for NIP-44 decryption'); } } }; // Export the API window.NOSTR_LOGIN_LITE = { init: NostrLite.init.bind(NostrLite), launch: NostrLite.launch.bind(NostrLite), logout: NostrLite.logout.bind(NostrLite), setDarkMode: NostrLite.setDarkMode.bind(NostrLite), setAuth: NostrLite.setAuth.bind(NostrLite), cancelNeedAuth: NostrLite.cancelNeedAuth.bind(NostrLite), // Expose internal components for debugging get _extensionBridge() { return LiteState.extensionBridge; }, get _state() { return LiteState; } }; // Set window.nostr facade properly (extensions will be handled by ExtensionBridge) if (typeof window !== 'undefined') { window.nostr = windowNostr; // Ensure all methods are properly exposed console.log('NOSTR_LOGIN_LITE: window.nostr facade installed with methods:', Object.keys(windowNostr)); } console.log('NOSTR_LOGIN_LITE loaded - use window.NOSTR_LOGIN_LITE.init(options) to initialize');