From bad361a6862eb33157a7dc0673009eee4937daf7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 13 Sep 2025 10:22:57 -0400 Subject: [PATCH] last commit before bundling nostr-tools ourselves --- lite/nostr-login-lite.bundle.js | 378 ++++++++++++++++++++++++-------- lite/nostr-login-lite.js | 181 ++++++++++++--- lite/ui/modal.js | 195 +++++++++++----- 3 files changed, 579 insertions(+), 175 deletions(-) diff --git a/lite/nostr-login-lite.bundle.js b/lite/nostr-login-lite.bundle.js index 6f55a95..e5274e2 100644 --- a/lite/nostr-login-lite.bundle.js +++ b/lite/nostr-login-lite.bundle.js @@ -1,7 +1,7 @@ /** * NOSTR_LOGIN_LITE * Single-file Nostr authentication library - * Generated on: 2025-09-13T13:03:05.960Z + * Generated on: 2025-09-13T13:54:03.650Z */ // ====================================== @@ -291,85 +291,166 @@ class Modal { // Detect all available real extensions const availableExtensions = this._detectAllExtensions(); + console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName)); + if (availableExtensions.length === 0) { console.log('Modal: No real extensions found'); this._showExtensionRequired(); } else if (availableExtensions.length === 1) { - console.log('Modal detected single extension:', availableExtensions[0].name); + // Single extension - use it directly without showing choice UI + console.log('Modal: Single extension detected, using it directly:', availableExtensions[0].displayName); this._tryExtensionLogin(availableExtensions[0].extension); } else { - console.log('Modal detected multiple extensions:', availableExtensions.map(e => e.name)); + // Multiple extensions - show choice UI + console.log('Modal: Multiple extensions detected, showing choice UI for', availableExtensions.length, 'extensions'); this._showExtensionChoice(availableExtensions); } } _detectAllExtensions() { const extensions = []; + const seenExtensions = new Set(); // Track extensions by object reference to avoid duplicates - // Check navigator.nostr (NIP-07 standard location) - if (window.navigator?.nostr && typeof window.navigator.nostr.getPublicKey === 'function') { - extensions.push({ - name: 'navigator.nostr', - displayName: 'Standard Extension (navigator.nostr)', - icon: '🌐', - extension: window.navigator.nostr - }); - } - - // Check webln.nostr (Alby WebLN) - if (window.webln?.nostr && typeof window.webln.nostr.getPublicKey === 'function') { - extensions.push({ - name: 'webln.nostr', - displayName: 'Alby WebLN Extension', - icon: '⚡', - extension: window.webln.nostr - }); - } - - // Check alby.nostr (Alby direct) - if (window.alby?.nostr && typeof window.alby.nostr.getPublicKey === 'function') { - extensions.push({ - name: 'alby.nostr', - displayName: 'Alby Extension (Direct)', - icon: '🐝', - extension: window.alby.nostr - }); - } - - // Check nos2x - if (window.nos2x && typeof window.nos2x.getPublicKey === 'function') { - extensions.push({ - name: 'nos2x', - displayName: 'nos2x Extension', - icon: '🔌', - extension: window.nos2x - }); - } - - // Check window.nostr but make sure it's not our library - if (window.nostr && typeof window.nostr.getPublicKey === 'function') { - const isRealExtension = ( - typeof window.nostr._hexToUint8Array !== 'function' && // Our library has this method - window.nostr.constructor.name !== 'Object' // Real extensions usually have proper constructors - ); - - if (isRealExtension) { - // Don't add if we already detected it via another path - const alreadyDetected = extensions.some(ext => ext.extension === window.nostr); - if (!alreadyDetected) { + // Extension locations to check (in priority order) + const locations = [ + { path: 'window.navigator?.nostr', name: 'navigator.nostr', displayName: 'Standard Extension (navigator.nostr)', icon: '🌐', getter: () => window.navigator?.nostr }, + { path: 'window.webln?.nostr', name: 'webln.nostr', displayName: 'Alby WebLN Extension', icon: '⚡', getter: () => window.webln?.nostr }, + { path: 'window.alby?.nostr', name: 'alby.nostr', displayName: 'Alby Extension (Direct)', icon: '🐝', getter: () => window.alby?.nostr }, + { path: 'window.nos2x', name: 'nos2x', displayName: 'nos2x Extension', icon: '🔌', getter: () => window.nos2x }, + { path: 'window.flamingo?.nostr', name: 'flamingo.nostr', displayName: 'Flamingo Extension', icon: '🦩', getter: () => window.flamingo?.nostr }, + { path: 'window.mutiny?.nostr', name: 'mutiny.nostr', displayName: 'Mutiny Extension', icon: '⚔️', getter: () => window.mutiny?.nostr }, + { path: 'window.nostrich?.nostr', name: 'nostrich.nostr', displayName: 'Nostrich Extension', icon: '🐦', getter: () => window.nostrich?.nostr }, + { path: 'window.getAlby?.nostr', name: 'getAlby.nostr', displayName: 'getAlby Extension', icon: '🔧', getter: () => window.getAlby?.nostr } + ]; + + // Check each location + for (const location of locations) { + try { + const obj = location.getter(); + + console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name); + + if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) { extensions.push({ - name: 'window.nostr', - displayName: 'Extension (window.nostr)', - icon: '🔑', - extension: window.nostr + name: location.name, + displayName: location.displayName, + icon: location.icon, + extension: obj }); + seenExtensions.add(obj); + console.log(`Modal: ✓ Detected extension at ${location.name} (${obj.constructor?.name})`); + } else if (obj) { + console.log(`Modal: ✗ Filtered out ${location.name} (${obj.constructor?.name})`); } + } catch (e) { + // Location doesn't exist or can't be accessed + console.log(`Modal: ${location.name} not accessible:`, e.message); } } + // Also check window.nostr but be extra careful to avoid our library + console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name); + if (window.nostr && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) { + extensions.push({ + name: 'window.nostr', + displayName: 'Extension (window.nostr)', + icon: '🔑', + extension: window.nostr + }); + seenExtensions.add(window.nostr); + console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`); + } else if (window.nostr) { + console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`); + } + return extensions; } + _isRealExtension(obj) { + console.log(`Modal: EXTENSIVE DEBUG - _isRealExtension called with:`, obj); + console.log(`Modal: Object type: ${typeof obj}`); + console.log(`Modal: Object truthy: ${!!obj}`); + + if (!obj || typeof obj !== 'object') { + console.log(`Modal: REJECT - Not an object`); + return false; + } + + console.log(`Modal: getPublicKey type: ${typeof obj.getPublicKey}`); + console.log(`Modal: signEvent type: ${typeof obj.signEvent}`); + + // Must have required Nostr methods + if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { + console.log(`Modal: REJECT - Missing required methods`); + return false; + } + + // Exclude NostrTools library object + if (obj === window.NostrTools) { + console.log(`Modal: REJECT - Is NostrTools object`); + return false; + } + + // Use the EXACT SAME logic as the comprehensive test (lines 804-809) + // This is the key fix - match the comprehensive test's successful detection logic + const constructorName = obj.constructor?.name; + const objectKeys = Object.keys(obj); + + console.log(`Modal: Constructor name: "${constructorName}"`); + console.log(`Modal: Object keys: [${objectKeys.join(', ')}]`); + + // COMPREHENSIVE TEST LOGIC - Accept anything with required methods that's not our specific library classes + const isRealExtension = ( + typeof obj.getPublicKey === 'function' && + typeof obj.signEvent === 'function' && + constructorName !== 'WindowNostr' && // Our library class + constructorName !== 'NostrLite' // Our main class + ); + + console.log(`Modal: Using comprehensive test logic:`); + console.log(` Has getPublicKey: ${typeof obj.getPublicKey === 'function'}`); + console.log(` Has signEvent: ${typeof obj.signEvent === 'function'}`); + console.log(` Not WindowNostr: ${constructorName !== 'WindowNostr'}`); + console.log(` Not NostrLite: ${constructorName !== 'NostrLite'}`); + console.log(` Constructor: "${constructorName}"`); + + // Additional debugging for comparison + const extensionPropChecks = { + _isEnabled: !!obj._isEnabled, + enabled: !!obj.enabled, + kind: !!obj.kind, + _eventEmitter: !!obj._eventEmitter, + _scope: !!obj._scope, + _requests: !!obj._requests, + _pubkey: !!obj._pubkey, + name: !!obj.name, + version: !!obj.version, + description: !!obj.description + }; + + console.log(`Modal: Extension property analysis:`, extensionPropChecks); + + const hasExtensionProps = !!( + obj._isEnabled || obj.enabled || obj.kind || + obj._eventEmitter || obj._scope || obj._requests || obj._pubkey || + obj.name || obj.version || obj.description + ); + + const underscoreKeys = objectKeys.filter(key => key.startsWith('_')); + const hexToUint8Keys = objectKeys.filter(key => key.startsWith('_hex')); + console.log(`Modal: Underscore keys: [${underscoreKeys.join(', ')}]`); + console.log(`Modal: _hex* keys: [${hexToUint8Keys.join(', ')}]`); + + console.log(`Modal: Additional analysis:`); + console.log(` hasExtensionProps: ${hasExtensionProps}`); + console.log(` hasLibraryMethod (_hexToUint8Array): ${objectKeys.includes('_hexToUint8Array')}`); + + console.log(`Modal: COMPREHENSIVE TEST LOGIC RESULT: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); + console.log(`Modal: FINAL DECISION for ${constructorName}: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); + + return isRealExtension; + } + _showExtensionChoice(extensions) { this.modalBody.innerHTML = ''; @@ -1684,7 +1765,8 @@ class ExtensionBridge { this.checking = false; this.checkInterval = null; this.originalNostr = null; - this.foundExtension = null; + this.foundExtensions = new Map(); // Store multiple extensions by location + this.primaryExtension = null; // The currently selected extension } startChecking(nostrLite) { @@ -1692,7 +1774,7 @@ class ExtensionBridge { this.checking = true; const check = () => { - this.initExtension(nostrLite); + this.detectAllExtensions(nostrLite); }; // Check immediately @@ -1708,50 +1790,170 @@ class ExtensionBridge { }, 30000); } - initExtension(nostrLite, lastTry = false) { - const extension = window.nostr; + 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 } + ]; - if (extension && !this.foundExtension) { - // Check if this is actually a real extension, not our own library - const isRealExtension = ( - extension !== nostrLite && // Not the same object we're about to assign - extension !== windowNostr && // Not our windowNostr object - typeof extension._hexToUint8Array !== 'function' && // Our library has this internal method - extension.constructor.name !== 'Object' // Real extensions usually have proper constructors + 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 (isRealExtension) { - this.foundExtension = extension; + 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; + } + } - // Cache the extension and reassign window.nostr to our lite version - this.originalNostr = window.nostr; + // 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('Real Nostr extension detected and bridged:', extension.constructor.name); + console.log(`Primary extension set: ${this.primaryExtension.name}`); - // If currently authenticated, reconcile state - if (LiteState.auth?.signer?.method === 'extension') { - this.reconcileExtension(); - } - } else { - console.log('Skipping non-extension object on window.nostr:', extension.constructor.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.foundExtension; + 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.foundExtension) return false; + if (!this.primaryExtension) return false; try { // Temporarily set window.nostr to extension const temp = window.nostr; - window.nostr = this.foundExtension; + window.nostr = this.primaryExtension.extension; - const pubkey = await this.foundExtension.getPublicKey(); + const pubkey = await this.primaryExtension.extension.getPublicKey(); // Restore our lite implementation window.nostr = temp; @@ -1780,8 +1982,8 @@ class ExtensionBridge { } setExtension() { - if (!this.foundExtension) return; - window.nostr = this.foundExtension; + if (!this.primaryExtension) return; + window.nostr = this.primaryExtension.extension; this.setExtensionReadPubkey().then(pubkey => { if (pubkey) { LiteState.bus?.emit('extensionSet', { pubkey }); @@ -1877,7 +2079,7 @@ class NostrLite { // Handle extension detection this.bus?.on('extensionDetected', (extension) => { console.log('Extension detected'); - LiteState.extensionBridge.foundExtension = extension; + LiteState.extensionBridge.setPrimaryExtension(extension, 'detected'); }); // Handle auth URL from NIP-46 @@ -1915,7 +2117,7 @@ class NostrLite { case 'extension': if (pubkey && extension) { // Store the extension object in the ExtensionBridge for future use - LiteState.extensionBridge.foundExtension = extension; + LiteState.extensionBridge.setPrimaryExtension(extension, 'modal-selected'); LiteState.extensionBridge.originalNostr = extension; // Set up extension authentication diff --git a/lite/nostr-login-lite.js b/lite/nostr-login-lite.js index 7ce1839..c2a6fee 100644 --- a/lite/nostr-login-lite.js +++ b/lite/nostr-login-lite.js @@ -264,7 +264,8 @@ class ExtensionBridge { this.checking = false; this.checkInterval = null; this.originalNostr = null; - this.foundExtension = null; + this.foundExtensions = new Map(); // Store multiple extensions by location + this.primaryExtension = null; // The currently selected extension } startChecking(nostrLite) { @@ -272,7 +273,7 @@ class ExtensionBridge { this.checking = true; const check = () => { - this.initExtension(nostrLite); + this.detectAllExtensions(nostrLite); }; // Check immediately @@ -288,50 +289,170 @@ class ExtensionBridge { }, 30000); } - initExtension(nostrLite, lastTry = false) { - const extension = window.nostr; + 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 } + ]; - if (extension && !this.foundExtension) { - // Check if this is actually a real extension, not our own library - const isRealExtension = ( - extension !== nostrLite && // Not the same object we're about to assign - extension !== windowNostr && // Not our windowNostr object - typeof extension._hexToUint8Array !== 'function' && // Our library has this internal method - extension.constructor.name !== 'Object' // Real extensions usually have proper constructors + 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 (isRealExtension) { - this.foundExtension = extension; + 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; + } + } - // Cache the extension and reassign window.nostr to our lite version - this.originalNostr = window.nostr; + // 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('Real Nostr extension detected and bridged:', extension.constructor.name); + console.log(`Primary extension set: ${this.primaryExtension.name}`); - // If currently authenticated, reconcile state - if (LiteState.auth?.signer?.method === 'extension') { - this.reconcileExtension(); - } - } else { - console.log('Skipping non-extension object on window.nostr:', extension.constructor.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.foundExtension; + 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.foundExtension) return false; + if (!this.primaryExtension) return false; try { // Temporarily set window.nostr to extension const temp = window.nostr; - window.nostr = this.foundExtension; + window.nostr = this.primaryExtension.extension; - const pubkey = await this.foundExtension.getPublicKey(); + const pubkey = await this.primaryExtension.extension.getPublicKey(); // Restore our lite implementation window.nostr = temp; @@ -360,8 +481,8 @@ class ExtensionBridge { } setExtension() { - if (!this.foundExtension) return; - window.nostr = this.foundExtension; + if (!this.primaryExtension) return; + window.nostr = this.primaryExtension.extension; this.setExtensionReadPubkey().then(pubkey => { if (pubkey) { LiteState.bus?.emit('extensionSet', { pubkey }); @@ -457,7 +578,7 @@ class NostrLite { // Handle extension detection this.bus?.on('extensionDetected', (extension) => { console.log('Extension detected'); - LiteState.extensionBridge.foundExtension = extension; + LiteState.extensionBridge.setPrimaryExtension(extension, 'detected'); }); // Handle auth URL from NIP-46 @@ -495,7 +616,7 @@ class NostrLite { case 'extension': if (pubkey && extension) { // Store the extension object in the ExtensionBridge for future use - LiteState.extensionBridge.foundExtension = extension; + LiteState.extensionBridge.setPrimaryExtension(extension, 'modal-selected'); LiteState.extensionBridge.originalNostr = extension; // Set up extension authentication diff --git a/lite/ui/modal.js b/lite/ui/modal.js index 71cf7f5..6951eae 100644 --- a/lite/ui/modal.js +++ b/lite/ui/modal.js @@ -281,85 +281,166 @@ class Modal { // Detect all available real extensions const availableExtensions = this._detectAllExtensions(); + console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName)); + if (availableExtensions.length === 0) { console.log('Modal: No real extensions found'); this._showExtensionRequired(); } else if (availableExtensions.length === 1) { - console.log('Modal detected single extension:', availableExtensions[0].name); + // Single extension - use it directly without showing choice UI + console.log('Modal: Single extension detected, using it directly:', availableExtensions[0].displayName); this._tryExtensionLogin(availableExtensions[0].extension); } else { - console.log('Modal detected multiple extensions:', availableExtensions.map(e => e.name)); + // Multiple extensions - show choice UI + console.log('Modal: Multiple extensions detected, showing choice UI for', availableExtensions.length, 'extensions'); this._showExtensionChoice(availableExtensions); } } _detectAllExtensions() { const extensions = []; + const seenExtensions = new Set(); // Track extensions by object reference to avoid duplicates - // Check navigator.nostr (NIP-07 standard location) - if (window.navigator?.nostr && typeof window.navigator.nostr.getPublicKey === 'function') { - extensions.push({ - name: 'navigator.nostr', - displayName: 'Standard Extension (navigator.nostr)', - icon: '🌐', - extension: window.navigator.nostr - }); - } - - // Check webln.nostr (Alby WebLN) - if (window.webln?.nostr && typeof window.webln.nostr.getPublicKey === 'function') { - extensions.push({ - name: 'webln.nostr', - displayName: 'Alby WebLN Extension', - icon: '⚡', - extension: window.webln.nostr - }); - } - - // Check alby.nostr (Alby direct) - if (window.alby?.nostr && typeof window.alby.nostr.getPublicKey === 'function') { - extensions.push({ - name: 'alby.nostr', - displayName: 'Alby Extension (Direct)', - icon: '🐝', - extension: window.alby.nostr - }); - } - - // Check nos2x - if (window.nos2x && typeof window.nos2x.getPublicKey === 'function') { - extensions.push({ - name: 'nos2x', - displayName: 'nos2x Extension', - icon: '🔌', - extension: window.nos2x - }); - } - - // Check window.nostr but make sure it's not our library - if (window.nostr && typeof window.nostr.getPublicKey === 'function') { - const isRealExtension = ( - typeof window.nostr._hexToUint8Array !== 'function' && // Our library has this method - window.nostr.constructor.name !== 'Object' // Real extensions usually have proper constructors - ); - - if (isRealExtension) { - // Don't add if we already detected it via another path - const alreadyDetected = extensions.some(ext => ext.extension === window.nostr); - if (!alreadyDetected) { + // Extension locations to check (in priority order) + const locations = [ + { path: 'window.navigator?.nostr', name: 'navigator.nostr', displayName: 'Standard Extension (navigator.nostr)', icon: '🌐', getter: () => window.navigator?.nostr }, + { path: 'window.webln?.nostr', name: 'webln.nostr', displayName: 'Alby WebLN Extension', icon: '⚡', getter: () => window.webln?.nostr }, + { path: 'window.alby?.nostr', name: 'alby.nostr', displayName: 'Alby Extension (Direct)', icon: '🐝', getter: () => window.alby?.nostr }, + { path: 'window.nos2x', name: 'nos2x', displayName: 'nos2x Extension', icon: '🔌', getter: () => window.nos2x }, + { path: 'window.flamingo?.nostr', name: 'flamingo.nostr', displayName: 'Flamingo Extension', icon: '🦩', getter: () => window.flamingo?.nostr }, + { path: 'window.mutiny?.nostr', name: 'mutiny.nostr', displayName: 'Mutiny Extension', icon: '⚔️', getter: () => window.mutiny?.nostr }, + { path: 'window.nostrich?.nostr', name: 'nostrich.nostr', displayName: 'Nostrich Extension', icon: '🐦', getter: () => window.nostrich?.nostr }, + { path: 'window.getAlby?.nostr', name: 'getAlby.nostr', displayName: 'getAlby Extension', icon: '🔧', getter: () => window.getAlby?.nostr } + ]; + + // Check each location + for (const location of locations) { + try { + const obj = location.getter(); + + console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name); + + if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) { extensions.push({ - name: 'window.nostr', - displayName: 'Extension (window.nostr)', - icon: '🔑', - extension: window.nostr + name: location.name, + displayName: location.displayName, + icon: location.icon, + extension: obj }); + seenExtensions.add(obj); + console.log(`Modal: ✓ Detected extension at ${location.name} (${obj.constructor?.name})`); + } else if (obj) { + console.log(`Modal: ✗ Filtered out ${location.name} (${obj.constructor?.name})`); } + } catch (e) { + // Location doesn't exist or can't be accessed + console.log(`Modal: ${location.name} not accessible:`, e.message); } } + // Also check window.nostr but be extra careful to avoid our library + console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name); + if (window.nostr && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) { + extensions.push({ + name: 'window.nostr', + displayName: 'Extension (window.nostr)', + icon: '🔑', + extension: window.nostr + }); + seenExtensions.add(window.nostr); + console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`); + } else if (window.nostr) { + console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`); + } + return extensions; } + _isRealExtension(obj) { + console.log(`Modal: EXTENSIVE DEBUG - _isRealExtension called with:`, obj); + console.log(`Modal: Object type: ${typeof obj}`); + console.log(`Modal: Object truthy: ${!!obj}`); + + if (!obj || typeof obj !== 'object') { + console.log(`Modal: REJECT - Not an object`); + return false; + } + + console.log(`Modal: getPublicKey type: ${typeof obj.getPublicKey}`); + console.log(`Modal: signEvent type: ${typeof obj.signEvent}`); + + // Must have required Nostr methods + if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { + console.log(`Modal: REJECT - Missing required methods`); + return false; + } + + // Exclude NostrTools library object + if (obj === window.NostrTools) { + console.log(`Modal: REJECT - Is NostrTools object`); + return false; + } + + // Use the EXACT SAME logic as the comprehensive test (lines 804-809) + // This is the key fix - match the comprehensive test's successful detection logic + const constructorName = obj.constructor?.name; + const objectKeys = Object.keys(obj); + + console.log(`Modal: Constructor name: "${constructorName}"`); + console.log(`Modal: Object keys: [${objectKeys.join(', ')}]`); + + // COMPREHENSIVE TEST LOGIC - Accept anything with required methods that's not our specific library classes + const isRealExtension = ( + typeof obj.getPublicKey === 'function' && + typeof obj.signEvent === 'function' && + constructorName !== 'WindowNostr' && // Our library class + constructorName !== 'NostrLite' // Our main class + ); + + console.log(`Modal: Using comprehensive test logic:`); + console.log(` Has getPublicKey: ${typeof obj.getPublicKey === 'function'}`); + console.log(` Has signEvent: ${typeof obj.signEvent === 'function'}`); + console.log(` Not WindowNostr: ${constructorName !== 'WindowNostr'}`); + console.log(` Not NostrLite: ${constructorName !== 'NostrLite'}`); + console.log(` Constructor: "${constructorName}"`); + + // Additional debugging for comparison + const extensionPropChecks = { + _isEnabled: !!obj._isEnabled, + enabled: !!obj.enabled, + kind: !!obj.kind, + _eventEmitter: !!obj._eventEmitter, + _scope: !!obj._scope, + _requests: !!obj._requests, + _pubkey: !!obj._pubkey, + name: !!obj.name, + version: !!obj.version, + description: !!obj.description + }; + + console.log(`Modal: Extension property analysis:`, extensionPropChecks); + + const hasExtensionProps = !!( + obj._isEnabled || obj.enabled || obj.kind || + obj._eventEmitter || obj._scope || obj._requests || obj._pubkey || + obj.name || obj.version || obj.description + ); + + const underscoreKeys = objectKeys.filter(key => key.startsWith('_')); + const hexToUint8Keys = objectKeys.filter(key => key.startsWith('_hex')); + console.log(`Modal: Underscore keys: [${underscoreKeys.join(', ')}]`); + console.log(`Modal: _hex* keys: [${hexToUint8Keys.join(', ')}]`); + + console.log(`Modal: Additional analysis:`); + console.log(` hasExtensionProps: ${hasExtensionProps}`); + console.log(` hasLibraryMethod (_hexToUint8Array): ${objectKeys.includes('_hexToUint8Array')}`); + + console.log(`Modal: COMPREHENSIVE TEST LOGIC RESULT: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); + console.log(`Modal: FINAL DECISION for ${constructorName}: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`); + + return isRealExtension; + } + _showExtensionChoice(extensions) { this.modalBody.innerHTML = '';