From 517974699dd76e323564da9a646b45f818d760b3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 15 Sep 2025 13:51:41 -0400 Subject: [PATCH] browser extension signing fixed --- lite/build.js | 321 ++++++++++++++++++++++++++++++++++++++++---- lite/nostr-lite.js | 323 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 593 insertions(+), 51 deletions(-) diff --git a/lite/build.js b/lite/build.js index 92df2ac..f084773 100644 --- a/lite/build.js +++ b/lite/build.js @@ -1093,10 +1093,8 @@ class NostrLite { // Apply the selected theme (CSS-only) this.switchTheme(this.options.theme); - // Set up window.nostr facade if no extension detected - if (this.extensionBridge.getExtensionCount() === 0) { - this._setupWindowNostrFacade(); - } + // Always set up window.nostr facade to handle multiple extensions properly + this._setupWindowNostrFacade(); // Create modal during init (matching original git architecture) this.modal = new Modal(this.options); @@ -1115,9 +1113,14 @@ class NostrLite { } _setupWindowNostrFacade() { - if (typeof window !== 'undefined' && !window.nostr) { - window.nostr = new WindowNostr(this); - console.log('NOSTR_LOGIN_LITE: window.nostr facade installed'); + if (typeof window !== 'undefined') { + // Store existing window.nostr if it exists (from extensions) + const existingNostr = window.nostr; + + // Always install our facade + window.nostr = new WindowNostr(this, existingNostr); + console.log('NOSTR_LOGIN_LITE: window.nostr facade installed', + existingNostr ? '(with extension passthrough)' : '(no existing extension)'); } } @@ -1231,45 +1234,313 @@ class NostrLite { } } -// Window.nostr facade for when no extension is available +// NIP-07 compliant window.nostr provider class WindowNostr { - constructor(nostrLite) { + constructor(nostrLite, existingNostr = null) { this.nostrLite = nostrLite; + this.authState = null; + this.existingNostr = existingNostr; + this.authenticatedExtension = null; + this._setupEventListeners(); } - + + _setupEventListeners() { + // Listen for authentication events to store auth state + if (typeof window !== 'undefined') { + window.addEventListener('nlMethodSelected', (event) => { + this.authState = event.detail; + + // If extension method, capture the specific extension the user chose + if (event.detail.method === 'extension') { + this.authenticatedExtension = event.detail.extension; + console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); + + // Re-install our facade to ensure we intercept signEvent calls + // Extensions may overwrite window.nostr after authentication + if (typeof window !== 'undefined') { + console.log('WindowNostr: Re-installing facade after authentication'); + window.nostr = this; + } + } + + console.log('WindowNostr: Auth state updated:', this.authState?.method); + }); + + window.addEventListener('nlLogout', () => { + this.authState = null; + this.authenticatedExtension = null; + console.log('WindowNostr: Auth state cleared'); + }); + } + } + async getPublicKey() { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + 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) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + if (this.authState.method === 'readonly') { + throw new Error('Read-only mode - cannot sign events'); + } + + switch (this.authState.method) { + case 'extension': + // Use the captured authenticated extension, not current window.nostr + console.log('WindowNostr: signEvent - authenticatedExtension:', this.authenticatedExtension); + console.log('WindowNostr: signEvent - authState.extension:', this.authState.extension); + console.log('WindowNostr: signEvent - existingNostr:', this.existingNostr); + + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + console.log('WindowNostr: signEvent - using extension:', ext); + console.log('WindowNostr: signEvent - extension constructor:', ext?.constructor?.name); + + if (!ext) throw new Error('Extension not available'); + return await ext.signEvent(event); + + case 'local': { + // Use nostr-tools to sign with local secret key + const { nip19, finalizeEvent } = window.NostrTools; + let secretKey; + + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); + secretKey = decoded.data; + } else { + // Convert hex to Uint8Array + secretKey = this._hexToUint8Array(this.authState.secret); + } + + return finalizeEvent(event, secretKey); + } + + case 'nip46': { + // Use BunkerSigner for NIP-46 + if (!this.authState.signer?.bunkerSigner) { + throw new Error('NIP-46 signer not available'); + } + return await this.authState.signer.bunkerSigner.signEvent(event); + } + + default: + throw new Error(\`Unsupported auth method: \${this.authState.method}\`); + } } - + async getRelays() { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + // Return configured relays from nostr-lite options + return this.nostrLite.options?.relays || ['wss://relay.damus.io']; } - + get nip04() { return { - async encrypt(pubkey, plaintext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + 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}\`); + } }, - async decrypt(pubkey, ciphertext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + + 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 { - async encrypt(pubkey, plaintext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + 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}\`); + } }, - async decrypt(pubkey, ciphertext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + + 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 diff --git a/lite/nostr-lite.js b/lite/nostr-lite.js index 4a30a44..0bcbeb2 100644 --- a/lite/nostr-lite.js +++ b/lite/nostr-lite.js @@ -8,7 +8,7 @@ * Two-file architecture: * 1. Load nostr.bundle.js (official nostr-tools bundle) * 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes) - * Generated on: 2025-09-14T20:06:51.995Z + * Generated on: 2025-09-15T17:48:16.817Z */ // Verify dependencies are loaded @@ -2401,10 +2401,8 @@ class NostrLite { // Apply the selected theme (CSS-only) this.switchTheme(this.options.theme); - // Set up window.nostr facade if no extension detected - if (this.extensionBridge.getExtensionCount() === 0) { - this._setupWindowNostrFacade(); - } + // Always set up window.nostr facade to handle multiple extensions properly + this._setupWindowNostrFacade(); // Create modal during init (matching original git architecture) this.modal = new Modal(this.options); @@ -2423,9 +2421,14 @@ class NostrLite { } _setupWindowNostrFacade() { - if (typeof window !== 'undefined' && !window.nostr) { - window.nostr = new WindowNostr(this); - console.log('NOSTR_LOGIN_LITE: window.nostr facade installed'); + if (typeof window !== 'undefined') { + // Store existing window.nostr if it exists (from extensions) + const existingNostr = window.nostr; + + // Always install our facade + window.nostr = new WindowNostr(this, existingNostr); + console.log('NOSTR_LOGIN_LITE: window.nostr facade installed', + existingNostr ? '(with extension passthrough)' : '(no existing extension)'); } } @@ -2539,45 +2542,313 @@ class NostrLite { } } -// Window.nostr facade for when no extension is available +// NIP-07 compliant window.nostr provider class WindowNostr { - constructor(nostrLite) { + constructor(nostrLite, existingNostr = null) { this.nostrLite = nostrLite; + this.authState = null; + this.existingNostr = existingNostr; + this.authenticatedExtension = null; + this._setupEventListeners(); } - + + _setupEventListeners() { + // Listen for authentication events to store auth state + if (typeof window !== 'undefined') { + window.addEventListener('nlMethodSelected', (event) => { + this.authState = event.detail; + + // If extension method, capture the specific extension the user chose + if (event.detail.method === 'extension') { + this.authenticatedExtension = event.detail.extension; + console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name); + + // Re-install our facade to ensure we intercept signEvent calls + // Extensions may overwrite window.nostr after authentication + if (typeof window !== 'undefined') { + console.log('WindowNostr: Re-installing facade after authentication'); + window.nostr = this; + } + } + + console.log('WindowNostr: Auth state updated:', this.authState?.method); + }); + + window.addEventListener('nlLogout', () => { + this.authState = null; + this.authenticatedExtension = null; + console.log('WindowNostr: Auth state cleared'); + }); + } + } + async getPublicKey() { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + 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) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + if (!this.authState) { + throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()'); + } + + if (this.authState.method === 'readonly') { + throw new Error('Read-only mode - cannot sign events'); + } + + switch (this.authState.method) { + case 'extension': + // Use the captured authenticated extension, not current window.nostr + console.log('WindowNostr: signEvent - authenticatedExtension:', this.authenticatedExtension); + console.log('WindowNostr: signEvent - authState.extension:', this.authState.extension); + console.log('WindowNostr: signEvent - existingNostr:', this.existingNostr); + + const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr; + console.log('WindowNostr: signEvent - using extension:', ext); + console.log('WindowNostr: signEvent - extension constructor:', ext?.constructor?.name); + + if (!ext) throw new Error('Extension not available'); + return await ext.signEvent(event); + + case 'local': { + // Use nostr-tools to sign with local secret key + const { nip19, finalizeEvent } = window.NostrTools; + let secretKey; + + if (this.authState.secret.startsWith('nsec')) { + const decoded = nip19.decode(this.authState.secret); + secretKey = decoded.data; + } else { + // Convert hex to Uint8Array + secretKey = this._hexToUint8Array(this.authState.secret); + } + + return finalizeEvent(event, secretKey); + } + + case 'nip46': { + // Use BunkerSigner for NIP-46 + if (!this.authState.signer?.bunkerSigner) { + throw new Error('NIP-46 signer not available'); + } + return await this.authState.signer.bunkerSigner.signEvent(event); + } + + default: + throw new Error(`Unsupported auth method: ${this.authState.method}`); + } } - + async getRelays() { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + // Return configured relays from nostr-lite options + return this.nostrLite.options?.relays || ['wss://relay.damus.io']; } - + get nip04() { return { - async encrypt(pubkey, plaintext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + 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}`); + } }, - async decrypt(pubkey, ciphertext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + + 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 { - async encrypt(pubkey, plaintext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + 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}`); + } }, - async decrypt(pubkey, ciphertext) { - throw new Error('Authentication required - use NOSTR_LOGIN_LITE.launch()'); + + 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