/** * NOSTR_LOGIN_LITE - Authentication Library * Two-file architecture: * 1. Load nostr.bundle.js (official nostr-tools bundle) * 2. Load nostr-lite.js (this file - consolidated NOSTR_LOGIN_LITE library with NIP-46) * Generated on: 2025-09-13T18:23:00.000Z */ // 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)); } // ====================================== // NIP-46 Extension (formerly nip46-extension.js) // ====================================== (function() { 'use strict'; // Check if NostrTools is available if (typeof window.NostrTools === 'undefined') { console.error('NIP-46 Extension requires nostr-tools to be loaded first'); return; } const { nip44, generateSecretKey, getPublicKey, finalizeEvent, verifyEvent, utils } = window.NostrTools; // NIP-05 regex for parsing const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$/; const BUNKER_REGEX = /^bunker:\/\/([0-9a-f]{64})\??([?\/\w:.=&%-]*)$/; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Event kinds const NostrConnect = 24133; const ClientAuth = 22242; const Handlerinformation = 31990; // Fetch implementation let _fetch; try { _fetch = fetch; } catch { _fetch = null; } function useFetchImplementation(fetchImplementation) { _fetch = fetchImplementation; } // Simple Pool implementation for NIP-46 class SimplePool { constructor() { this.relays = new Map(); this.subscriptions = new Map(); } async ensureRelay(url) { if (!this.relays.has(url)) { console.log(`NIP-46: Connecting to relay ${url}`); const ws = new WebSocket(url); const relay = { ws, connected: false, subscriptions: new Map() }; this.relays.set(url, relay); // Wait for connection with proper event handlers await new Promise((resolve, reject) => { const timeout = setTimeout(() => { console.error(`NIP-46: Connection timeout for ${url}`); reject(new Error(`Connection timeout to ${url}`)); }, 10000); // 10 second timeout ws.onopen = () => { console.log(`NIP-46: Successfully connected to relay ${url}, WebSocket state: ${ws.readyState}`); relay.connected = true; clearTimeout(timeout); resolve(); }; ws.onerror = (error) => { console.error(`NIP-46: Failed to connect to ${url}:`, error); clearTimeout(timeout); reject(new Error(`Failed to connect to ${url}: ${error.message || 'Connection failed'}`)); }; ws.onclose = (event) => { console.log(`NIP-46: Disconnected from relay ${url}:`, event.code, event.reason); relay.connected = false; if (this.relays.has(url)) { this.relays.delete(url); } clearTimeout(timeout); reject(new Error(`Connection closed during setup: ${event.reason || 'Unknown reason'}`)); }; }); } else { const relay = this.relays.get(url); // Verify the existing connection is still open if (!relay.connected || relay.ws.readyState !== WebSocket.OPEN) { console.log(`NIP-46: Reconnecting to relay ${url}`); this.relays.delete(url); return await this.ensureRelay(url); // Recursively reconnect } } const relay = this.relays.get(url); console.log(`NIP-46: Relay ${url} ready, WebSocket state: ${relay.ws.readyState}`); return relay; } subscribe(relays, filters, params = {}) { const subId = Math.random().toString(36).substring(7); relays.forEach(async (url) => { try { const relay = await this.ensureRelay(url); relay.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data[0] === 'EVENT' && data[1] === subId) { params.onevent?.(data[2]); } else if (data[0] === 'EOSE' && data[1] === subId) { params.oneose?.(); } } catch (err) { console.warn('Failed to parse message:', err); } }; // Ensure filters is an array const filtersArray = Array.isArray(filters) ? filters : [filters]; const reqMsg = JSON.stringify(['REQ', subId, ...filtersArray]); relay.ws.send(reqMsg); } catch (err) { console.warn('Failed to connect to relay:', url, err); } }); return { close: () => { relays.forEach(async (url) => { const relay = this.relays.get(url); if (relay?.connected) { relay.ws.send(JSON.stringify(['CLOSE', subId])); } }); } }; } async publish(relays, event) { console.log(`NIP-46: Publishing event to ${relays.length} relays:`, event); const promises = relays.map(async (url) => { try { console.log(`NIP-46: Attempting to publish to ${url}`); const relay = await this.ensureRelay(url); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { console.error(`NIP-46: Publish timeout to ${url}`); reject(new Error(`Publish timeout to ${url}`)); }, 10000); // Increased timeout to 10 seconds // Set up message handler for this specific event const messageHandler = (msg) => { try { const data = JSON.parse(msg.data); if (data[0] === 'OK' && data[1] === event.id) { clearTimeout(timeout); relay.ws.removeEventListener('message', messageHandler); if (data[2]) { console.log(`NIP-46: Publish success to ${url}:`, data[3]); resolve(data[3]); } else { console.error(`NIP-46: Publish rejected by ${url}:`, data[3]); reject(new Error(`Publish rejected: ${data[3]}`)); } } } catch (err) { console.error(`NIP-46: Error parsing message from ${url}:`, err); clearTimeout(timeout); relay.ws.removeEventListener('message', messageHandler); reject(err); } }; relay.ws.addEventListener('message', messageHandler); // Double-check WebSocket state before sending console.log(`NIP-46: About to publish to ${url}, WebSocket state: ${relay.ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)`); if (relay.ws.readyState === WebSocket.OPEN) { console.log(`NIP-46: Sending event to ${url}`); relay.ws.send(JSON.stringify(['EVENT', event])); } else { console.error(`NIP-46: WebSocket not ready for ${url}, state: ${relay.ws.readyState}`); clearTimeout(timeout); relay.ws.removeEventListener('message', messageHandler); reject(new Error(`WebSocket not ready for ${url}, state: ${relay.ws.readyState}`)); } }); } catch (err) { console.error(`NIP-46: Failed to publish to ${url}:`, err); return Promise.reject(new Error(`Failed to publish to ${url}: ${err.message}`)); } }); const results = await Promise.allSettled(promises); console.log(`NIP-46: Publish results:`, results); return results; } async querySync(relays, filter, params = {}) { return new Promise((resolve) => { const events = []; this.subscribe(relays, [filter], { ...params, onevent: (event) => events.push(event), oneose: () => resolve(events) }); }); } } // Bunker URL utilities function toBunkerURL(bunkerPointer) { let bunkerURL = new URL(`bunker://${bunkerPointer.pubkey}`); bunkerPointer.relays.forEach((relay) => { bunkerURL.searchParams.append('relay', relay); }); if (bunkerPointer.secret) { bunkerURL.searchParams.set('secret', bunkerPointer.secret); } return bunkerURL.toString(); } async function parseBunkerInput(input) { let match = input.match(BUNKER_REGEX); if (match) { try { const pubkey = match[1]; const qs = new URLSearchParams(match[2]); return { pubkey, relays: qs.getAll('relay'), secret: qs.get('secret') }; } catch (_err) { // Continue to NIP-05 parsing } } return queryBunkerProfile(input); } async function queryBunkerProfile(nip05) { if (!_fetch) { throw new Error('Fetch implementation not available'); } const match = nip05.match(NIP05_REGEX); if (!match) return null; const [_, name = '_', domain] = match; try { const url = `https://${domain}/.well-known/nostr.json?name=${name}`; const res = await (await _fetch(url, { redirect: 'error' })).json(); let pubkey = res.names[name]; let relays = res.nip46[pubkey] || []; return { pubkey, relays, secret: null }; } catch (_err) { return null; } } // BunkerSigner class class BunkerSigner { constructor(clientSecretKey, bp, params = {}) { if (bp.relays.length === 0) { throw new Error('no relays are specified for this bunker'); } this.params = params; this.pool = params.pool || new SimplePool(); this.secretKey = clientSecretKey; this.conversationKey = nip44.getConversationKey(clientSecretKey, bp.pubkey); this.bp = bp; this.isOpen = false; this.idPrefix = Math.random().toString(36).substring(7); this.serial = 0; this.listeners = {}; this.waitingForAuth = {}; this.ready = false; this.readyPromise = this.setupSubscription(params); } async setupSubscription(params) { console.log('NIP-46: Setting up subscription to relays:', this.bp.relays); const listeners = this.listeners; const waitingForAuth = this.waitingForAuth; const convKey = this.conversationKey; // Ensure all relays are connected first await Promise.all(this.bp.relays.map(url => this.pool.ensureRelay(url))); console.log('NIP-46: All relays connected, setting up subscription'); this.subCloser = this.pool.subscribe( this.bp.relays, [{ kinds: [NostrConnect], authors: [this.bp.pubkey], '#p': [getPublicKey(this.secretKey)] }], { onevent: async (event) => { const o = JSON.parse(nip44.decrypt(event.content, convKey)); const { id, result, error } = o; if (result === 'auth_url' && waitingForAuth[id]) { delete waitingForAuth[id]; if (params.onauth) { params.onauth(error); } else { console.warn( `NIP-46: remote signer ${this.bp.pubkey} tried to send an "auth_url"='${error}' but there was no onauth() callback configured.` ); } return; } let handler = listeners[id]; if (handler) { if (error) handler.reject(error); else if (result) handler.resolve(result); delete listeners[id]; } }, onclose: () => { this.subCloser = undefined; } } ); this.isOpen = true; this.ready = true; console.log('NIP-46: BunkerSigner setup complete and ready'); } async ensureReady() { if (!this.ready) { console.log('NIP-46: Waiting for BunkerSigner to be ready...'); await this.readyPromise; } } async close() { this.isOpen = false; this.subCloser?.close(); } async sendRequest(method, params) { return new Promise(async (resolve, reject) => { try { await this.ensureReady(); // Wait for BunkerSigner to be ready if (!this.isOpen) { throw new Error('this signer is not open anymore, create a new one'); } if (!this.subCloser) { await this.setupSubscription(this.params); } this.serial++; const id = `${this.idPrefix}-${this.serial}`; const encryptedContent = nip44.encrypt(JSON.stringify({ id, method, params }), this.conversationKey); const verifiedEvent = finalizeEvent( { kind: NostrConnect, tags: [['p', this.bp.pubkey]], content: encryptedContent, created_at: Math.floor(Date.now() / 1000) }, this.secretKey ); this.listeners[id] = { resolve, reject }; this.waitingForAuth[id] = true; console.log(`NIP-46: Sending ${method} request with id ${id}`); const publishResults = await this.pool.publish(this.bp.relays, verifiedEvent); // Check if at least one publish succeeded const hasSuccess = publishResults.some(result => result.status === 'fulfilled'); if (!hasSuccess) { throw new Error('Failed to publish to any relay'); } console.log(`NIP-46: ${method} request sent successfully`); } catch (err) { console.error(`NIP-46: sendRequest ${method} failed:`, err); reject(err); } }); } async ping() { let resp = await this.sendRequest('ping', []); if (resp !== 'pong') { throw new Error(`result is not pong: ${resp}`); } } async connect() { await this.sendRequest('connect', [this.bp.pubkey, this.bp.secret || '']); } async getPublicKey() { if (!this.cachedPubKey) { this.cachedPubKey = await this.sendRequest('get_public_key', []); } return this.cachedPubKey; } async signEvent(event) { let resp = await this.sendRequest('sign_event', [JSON.stringify(event)]); let signed = JSON.parse(resp); if (verifyEvent(signed)) { return signed; } else { throw new Error(`event returned from bunker is improperly signed: ${JSON.stringify(signed)}`); } } async nip04Encrypt(thirdPartyPubkey, plaintext) { return await this.sendRequest('nip04_encrypt', [thirdPartyPubkey, plaintext]); } async nip04Decrypt(thirdPartyPubkey, ciphertext) { return await this.sendRequest('nip04_decrypt', [thirdPartyPubkey, ciphertext]); } async nip44Encrypt(thirdPartyPubkey, plaintext) { return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, plaintext]); } async nip44Decrypt(thirdPartyPubkey, ciphertext) { return await this.sendRequest('nip44_decrypt', [thirdPartyPubkey, ciphertext]); } } async function createAccount(bunker, params, username, domain, email, localSecretKey = generateSecretKey()) { if (email && !EMAIL_REGEX.test(email)) { throw new Error('Invalid email'); } let rpc = new BunkerSigner(localSecretKey, bunker.bunkerPointer, params); let pubkey = await rpc.sendRequest('create_account', [username, domain, email || '']); rpc.bp.pubkey = pubkey; await rpc.connect(); return rpc; } async function fetchBunkerProviders(pool, relays) { const events = await pool.querySync(relays, { kinds: [Handlerinformation], '#k': [NostrConnect.toString()] }); events.sort((a, b) => b.created_at - a.created_at); const validatedBunkers = await Promise.all( events.map(async (event, i) => { try { const content = JSON.parse(event.content); try { if (events.findIndex((ev) => JSON.parse(ev.content).nip05 === content.nip05) !== i) { return undefined; } } catch (err) { // Continue processing } const bp = await queryBunkerProfile(content.nip05); if (bp && bp.pubkey === event.pubkey && bp.relays.length) { return { bunkerPointer: bp, nip05: content.nip05, domain: content.nip05.split('@')[1], name: content.name || content.display_name, picture: content.picture, about: content.about, website: content.website, local: false }; } } catch (err) { return undefined; } }) ); return validatedBunkers.filter((b) => b !== undefined); } // Extend NostrTools with NIP-46 functionality window.NostrTools.nip46 = { BunkerSigner, parseBunkerInput, toBunkerURL, queryBunkerProfile, createAccount, fetchBunkerProviders, useFetchImplementation, BUNKER_REGEX, SimplePool }; console.log('NIP-46 extension loaded successfully (embedded)'); console.log('Available: NostrTools.nip46'); })(); // Verify NIP-46 extension is now available if (typeof window !== 'undefined') { console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46); } // ====================================== // NOSTR_LOGIN_LITE Components // ====================================== // ====================================== // Modal UI Component // ====================================== class Modal { constructor(options) { this.options = options; this.container = null; this.isVisible = false; this.currentScreen = null; // Initialize modal container and styles this._initModal(); } _initModal() { // Create modal container this.container = document.createElement('div'); this.container.id = 'nl-modal'; this.container.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.75); display: none; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // Create modal content const modalContent = document.createElement('div'); modalContent.style.cssText = ` position: relative; background: white; width: 90%; max-width: 400px; margin: 50px auto; border-radius: 12px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); max-height: 600px; overflow: hidden; `; // Header const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` padding: 20px 24px 0 24px; display: flex; justify-content: space-between; align-items: center; `; const modalTitle = document.createElement('h2'); modalTitle.textContent = 'Nostr Login'; modalTitle.style.cssText = ` margin: 0; font-size: 24px; font-weight: 600; color: #1f2937; `; const closeButton = document.createElement('button'); closeButton.innerHTML = 'ร'; closeButton.onclick = () => this.close(); closeButton.style.cssText = ` background: none; border: none; font-size: 28px; color: #6b7280; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; `; closeButton.onmouseover = () => closeButton.style.background = '#f3f4f6'; closeButton.onmouseout = () => closeButton.style.background = 'none'; modalHeader.appendChild(modalTitle); modalHeader.appendChild(closeButton); // Body this.modalBody = document.createElement('div'); this.modalBody.style.cssText = ` padding: 24px; overflow-y: auto; max-height: 500px; `; modalContent.appendChild(modalHeader); modalContent.appendChild(this.modalBody); this.container.appendChild(modalContent); // Add to body document.body.appendChild(this.container); // Click outside to close this.container.onclick = (e) => { if (e.target === this.container) { this.close(); } }; // Update theme this.updateTheme(); } updateTheme() { const isDark = this.options?.darkMode; const modalContent = this.container.querySelector(':nth-child(1)'); const title = this.container.querySelector('h2'); if (isDark) { modalContent.style.background = '#1f2937'; title.style.color = 'white'; } else { modalContent.style.background = 'white'; title.style.color = '#1f2937'; } } open(opts = {}) { this.currentScreen = opts.startScreen; this.isVisible = true; this.container.style.display = 'block'; // Render login options this._renderLoginOptions(); } close() { this.isVisible = false; this.container.style.display = 'none'; this.modalBody.innerHTML = ''; } _renderLoginOptions() { this.modalBody.innerHTML = ''; const options = []; // Extension option if (this.options?.methods?.extension !== false) { options.push({ type: 'extension', title: 'Browser Extension', description: 'Use your browser extension', icon: '๐' }); } // Local key option if (this.options?.methods?.local !== false) { options.push({ type: 'local', title: 'Local Key', description: 'Create or import your own key', icon: '๐' }); } // Nostr Connect option (check both 'connect' and 'remote' for compatibility) if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) { options.push({ type: 'connect', title: 'Nostr Connect', description: 'Connect with external signer', icon: '๐' }); } // Read-only option if (this.options?.methods?.readonly !== false) { options.push({ type: 'readonly', title: 'Read Only', description: 'Browse without signing', icon: '๐๏ธ' }); } // OTP/DM option if (this.options?.methods?.otp !== false) { options.push({ type: 'otp', title: 'DM/OTP', description: 'Receive OTP via DM', icon: '๐ฑ' }); } // Render each option options.forEach(option => { const button = document.createElement('button'); button.onclick = () => this._handleOptionClick(option.type); button.style.cssText = ` display: flex; align-items: center; width: 100%; padding: 16px; margin-bottom: 12px; background: ${this.options?.darkMode ? '#374151' : 'white'}; border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'}; border-radius: 8px; cursor: pointer; transition: all 0.2s; `; button.onmouseover = () => { button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)'; }; button.onmouseout = () => { button.style.boxShadow = 'none'; }; const iconDiv = document.createElement('div'); iconDiv.textContent = option.icon; iconDiv.style.cssText = ` font-size: 24px; margin-right: 16px; width: 24px; text-align: center; `; const contentDiv = document.createElement('div'); contentDiv.style.cssText = 'flex: 1; text-align: left;'; const titleDiv = document.createElement('div'); titleDiv.textContent = option.title; titleDiv.style.cssText = ` font-weight: 600; margin-bottom: 4px; color: ${this.options?.darkMode ? 'white' : '#1f2937'}; `; const descDiv = document.createElement('div'); descDiv.textContent = option.description; descDiv.style.cssText = ` font-size: 14px; color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'}; `; contentDiv.appendChild(titleDiv); contentDiv.appendChild(descDiv); button.appendChild(iconDiv); button.appendChild(contentDiv); this.modalBody.appendChild(button); }); } _handleOptionClick(type) { console.log('Selected login type:', type); // Handle different login types switch (type) { case 'extension': this._handleExtension(); break; case 'local': this._showLocalKeyScreen(); break; case 'connect': this._showConnectScreen(); break; case 'readonly': this._handleReadonly(); break; case 'otp': this._showOtpScreen(); break; } } _handleExtension() { // 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) { // 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 { // 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 // Extension locations to check (in priority order) const locations = [ { path: 'window.navigator?.nostr', name: 'navigator.nostr', displayName: 'Standard Extension (navigator.nostr)', icon: '๐', getter: () => window.navigator?.nostr }, { path: 'window.webln?.nostr', name: 'webln.nostr', displayName: 'Alby WebLN Extension', icon: 'โก', getter: () => window.webln?.nostr }, { path: 'window.alby?.nostr', name: 'alby.nostr', displayName: 'Alby Extension (Direct)', icon: '๐', getter: () => window.alby?.nostr }, { path: 'window.nos2x', name: 'nos2x', displayName: 'nos2x Extension', icon: '๐', getter: () => window.nos2x }, { path: 'window.flamingo?.nostr', name: 'flamingo.nostr', displayName: 'Flamingo Extension', icon: '๐ฆฉ', getter: () => window.flamingo?.nostr }, { path: 'window.mutiny?.nostr', name: 'mutiny.nostr', displayName: 'Mutiny Extension', icon: 'โ๏ธ', getter: () => window.mutiny?.nostr }, { path: 'window.nostrich?.nostr', name: 'nostrich.nostr', displayName: 'Nostrich Extension', icon: '๐ฆ', getter: () => window.nostrich?.nostr }, { path: 'window.getAlby?.nostr', name: 'getAlby.nostr', displayName: 'getAlby Extension', icon: '๐ง', getter: () => window.getAlby?.nostr } ]; // Check each location for (const location of locations) { try { const obj = location.getter(); console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name); if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) { extensions.push({ name: location.name, displayName: location.displayName, icon: location.icon, extension: obj }); seenExtensions.add(obj); console.log(`Modal: โ Detected extension at ${location.name} (${obj.constructor?.name})`); } else if (obj) { console.log(`Modal: โ Filtered out ${location.name} (${obj.constructor?.name})`); } } catch (e) { // Location doesn't exist or can't be accessed console.log(`Modal: ${location.name} not accessible:`, e.message); } } // Also check window.nostr but be extra careful to avoid our library console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name); if (window.nostr && 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 = ''; const title = document.createElement('h3'); title.textContent = 'Choose Browser Extension'; title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;'; const description = document.createElement('p'); description.textContent = `Found ${extensions.length} Nostr extensions. Choose which one to use:`; description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;'; this.modalBody.appendChild(title); this.modalBody.appendChild(description); // Create button for each extension extensions.forEach((ext, index) => { const button = document.createElement('button'); button.onclick = () => this._tryExtensionLogin(ext.extension); button.style.cssText = ` display: flex; align-items: center; width: 100%; padding: 16px; margin-bottom: 12px; background: ${this.options?.darkMode ? '#374151' : 'white'}; border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'}; border-radius: 8px; cursor: pointer; transition: all 0.2s; text-align: left; `; button.onmouseover = () => { button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)'; button.style.transform = 'translateY(-1px)'; }; button.onmouseout = () => { button.style.boxShadow = 'none'; button.style.transform = 'none'; }; const iconDiv = document.createElement('div'); iconDiv.textContent = ext.icon; iconDiv.style.cssText = ` font-size: 24px; margin-right: 16px; width: 24px; text-align: center; `; const contentDiv = document.createElement('div'); contentDiv.style.cssText = 'flex: 1;'; const nameDiv = document.createElement('div'); nameDiv.textContent = ext.displayName; nameDiv.style.cssText = ` font-weight: 600; margin-bottom: 4px; color: ${this.options?.darkMode ? 'white' : '#1f2937'}; `; const pathDiv = document.createElement('div'); pathDiv.textContent = ext.name; pathDiv.style.cssText = ` font-size: 12px; color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'}; font-family: monospace; `; contentDiv.appendChild(nameDiv); contentDiv.appendChild(pathDiv); button.appendChild(iconDiv); button.appendChild(contentDiv); this.modalBody.appendChild(button); }); // Add back button const backButton = document.createElement('button'); backButton.textContent = 'Back to Login Options'; backButton.onclick = () => this._renderLoginOptions(); backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 20px;'; this.modalBody.appendChild(backButton); } async _tryExtensionLogin(extensionObj) { try { // Show loading state this.modalBody.innerHTML = '
${nsec}`;
nsecDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
const npubDiv = document.createElement('div');
npubDiv.innerHTML = `Your Public Key:${window.NostrTools.nip19.npubEncode(pubkey)}`;
npubDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
const continueButton = document.createElement('button');
continueButton.textContent = 'Continue';
continueButton.onclick = () => this._setAuthMethod('local', { secret: nsec, pubkey });
continueButton.style.cssText = this._getButtonStyle();
this.modalBody.appendChild(title);
this.modalBody.appendChild(warningDiv);
this.modalBody.appendChild(nsecDiv);
this.modalBody.appendChild(npubDiv);
this.modalBody.appendChild(continueButton);
}
_setAuthMethod(method, options = {}) {
// Emit auth method selection
const event = new CustomEvent('nlMethodSelected', {
detail: { method, ...options }
});
window.dispatchEvent(event);
this.close();
}
_showError(message) {
this.modalBody.innerHTML = '';
const errorDiv = document.createElement('div');
errorDiv.style.cssText = 'background: #fee2e2; color: #dc2626; padding: 16px; border-radius: 6px; margin-bottom: 16px;';
errorDiv.innerHTML = `Error: ${message}`;
const backButton = document.createElement('button');
backButton.textContent = 'Back';
backButton.onclick = () => this._renderLoginOptions();
backButton.style.cssText = this._getButtonStyle('secondary');
this.modalBody.appendChild(errorDiv);
this.modalBody.appendChild(backButton);
}
_showExtensionRequired() {
this.modalBody.innerHTML = '';
const title = document.createElement('h3');
title.textContent = 'Browser Extension Required';
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
const message = document.createElement('p');
message.textContent = 'Please install a Nostr browser extension like Alby or getflattr and refresh the page.';
message.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
const backButton = document.createElement('button');
backButton.textContent = 'Back';
backButton.onclick = () => this._renderLoginOptions();
backButton.style.cssText = this._getButtonStyle('secondary');
this.modalBody.appendChild(title);
this.modalBody.appendChild(message);
this.modalBody.appendChild(backButton);
}
_showConnectScreen() {
this.modalBody.innerHTML = '';
const title = document.createElement('h3');
title.textContent = 'Nostr Connect';
title.style.cssText = 'margin: 0 0 20px 0; font-size: 18px; font-weight: 600;';
const formGroup = document.createElement('div');
formGroup.style.cssText = 'margin-bottom: 20px;';
const label = document.createElement('label');
label.textContent = 'Connection String:';
label.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
const pubkeyInput = document.createElement('input');
pubkeyInput.type = 'text';
pubkeyInput.placeholder = 'bunker://...';
pubkeyInput.style.cssText = `
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
margin-bottom: 16px;
font-family: monospace;
box-sizing: border-box;
`;
const connectButton = document.createElement('button');
connectButton.textContent = 'Connect';
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, null);
connectButton.style.cssText = this._getButtonStyle();
const backButton = document.createElement('button');
backButton.textContent = 'Back';
backButton.onclick = () => this._renderLoginOptions();
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
formGroup.appendChild(label);
formGroup.appendChild(pubkeyInput);
this.modalBody.appendChild(title);
this.modalBody.appendChild(formGroup);
this.modalBody.appendChild(connectButton);
this.modalBody.appendChild(backButton);
}
_handleNip46Connect(bunkerPubkey, bunkerUrl) {
if (!bunkerPubkey || !bunkerPubkey.length) {
this._showError('Bunker connection string is required');
return;
}
this._showNip46Connecting(bunkerPubkey);
this._performNip46Connect(bunkerPubkey, null);
}
_showNip46Connecting(bunkerPubkey) {
this.modalBody.innerHTML = '';
const title = document.createElement('h3');
title.textContent = 'Connecting to Remote Signer...';
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;';
const description = document.createElement('p');
description.textContent = 'Establishing secure connection to your remote signer via Nostr relays.';
description.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
// Show the connection string being used
const connectionString = bunkerPubkey.length > 60 ?
`${bunkerPubkey.substring(0, 30)}...${bunkerPubkey.substring(bunkerPubkey.length - 30)}` :
bunkerPubkey;
const bunkerInfo = document.createElement('div');
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
bunkerInfo.innerHTML = `
Connecting via:${connectionString}${npub}`;
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
const formGroup = document.createElement('div');
formGroup.style.cssText = 'margin-bottom: 20px;';
const label = document.createElement('label');
label.textContent = 'Login Code:';
label.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
const codeInput = document.createElement('input');
codeInput.type = 'text';
codeInput.placeholder = 'Enter 6-digit code';
codeInput.maxLength = 6;
codeInput.style.cssText = `
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
margin-bottom: 16px;
font-family: monospace;
box-sizing: border-box;
text-align: center;
font-size: 18px;
letter-spacing: 2px;
`;
const verifyButton = document.createElement('button');
verifyButton.textContent = 'Verify Code';
verifyButton.onclick = () => this._handleOtpVerification(pubkey, codeInput.value);
verifyButton.style.cssText = this._getButtonStyle();
const resendButton = document.createElement('button');
resendButton.textContent = 'Resend Code';
resendButton.onclick = () => this._handleOtpRequest(npub);
resendButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px; margin-right: 8px;';
const backButton = document.createElement('button');
backButton.textContent = 'Back';
backButton.onclick = () => this._showOtpScreen();
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
formGroup.appendChild(label);
formGroup.appendChild(codeInput);
this.modalBody.appendChild(title);
this.modalBody.appendChild(description);
this.modalBody.appendChild(formGroup);
this.modalBody.appendChild(verifyButton);
const buttonRow = document.createElement('div');
buttonRow.style.cssText = 'display: flex; gap: 8px;';
buttonRow.appendChild(resendButton);
buttonRow.appendChild(backButton);
this.modalBody.appendChild(buttonRow);
// Simulate sending DM (in a real implementation, this would send a DM)
this._simulateOtpSend(pubkey);
}
async _simulateOtpSend(pubkey) {
// Generate a random 6-digit code
const code = Math.floor(100000 + Math.random() * 900000).toString();
// Store the code temporarily (in a real implementation, this would be server-side)
sessionStorage.setItem(`otp_${pubkey}`, code);
sessionStorage.setItem(`otp_${pubkey}_timestamp`, Date.now().toString());
console.log(`OTP/DM: Generated code ${code} for pubkey ${pubkey}`);
// Actually send the DM
await this._sendOtpDm(pubkey, code);
}
async _sendOtpDm(recipientPubkey, code) {
try {
console.log('๐ OTP/DM: Starting modern NIP-17 DM send process...');
// Generate a temporary key pair for sending the DM
const senderSecretKey = window.NostrTools.generateSecretKey();
const senderPubkey = window.NostrTools.getPublicKey(senderSecretKey);
console.log('๐ OTP/DM: Generated temporary sender pubkey:', senderPubkey);
console.log('๐ค OTP/DM: Sending DM to recipient pubkey:', recipientPubkey);
// Create the DM content
const dmContent = `Your NOSTR_LOGIN_LITE login code: ${code}\n\nThis code expires in 5 minutes.`;
console.log('๐ฌ OTP/DM: Original message:', dmContent);
// Check if NIP-17 is available in nostr-tools
if (!window.NostrTools?.nip17) {
console.warn('โ ๏ธ OTP/DM: NIP-17 not available in nostr-tools, falling back to NIP-44');
return await this._sendLegacyNip44Dm(senderSecretKey, recipientPubkey, dmContent, code);
}
// Create recipient object for NIP-17
const recipient = {
publicKey: recipientPubkey,
relayUrl: undefined // Will use default relays
};
console.log('๐ OTP/DM: Creating NIP-17 gift-wrapped DM using nostr-tools...');
// Use nostr-tools NIP-17 to create properly wrapped DM
const wrappedEvent = window.NostrTools.nip17.wrapEvent(
senderSecretKey,
recipient,
dmContent,
undefined, // no conversation title
undefined // no reply
);
console.log('๐ OTP/DM: Created NIP-17 gift wrap (kind 1059):', JSON.stringify(wrappedEvent, null, 2));
// Get relays to send to
const relays = this.options.relays || [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://relay.laantungir.net',
'wss://nostr.mom'
];
console.log('๐ OTP/DM: Sending NIP-17 DM to relays:', relays);
// Create a pool using our enhanced SimplePool from NIP-46 extension
const pool = new window.NostrTools.nip46.SimplePool();
// Send the gift-wrapped event to all relays
console.log('๐ก OTP/DM: === Sending NIP-17 Gift-Wrapped DM ===');
const publishPromises = relays.map(async (relay, index) => {
console.log(`๐ก OTP/DM: NIP-17 [${index + 1}/${relays.length}] Connecting to relay ${relay}...`);
try {
const results = await pool.publish([relay], wrappedEvent);
console.log(`๐ก OTP/DM: NIP-17 Raw JSON response from ${relay}:`, JSON.stringify(results, null, 2));
// Check if the result status is fulfilled (successful)
const success = results.length > 0 && results[0].status === 'fulfilled';
if (!success && results.length > 0 && results[0].status === 'rejected') {
console.warn(`โ ๏ธ OTP/DM: NIP-17 rejected by ${relay}:`, results[0].reason);
}
return { relay, success, results, type: 'nip17' };
} catch (error) {
console.error(`โ OTP/DM: NIP-17 Failed to publish to ${relay}:`, error);
return { relay, success: false, error: error.message, type: 'nip17' };
}
});
// Wait for all publish attempts
const results = await Promise.allSettled(publishPromises);
const successfulResults = results.filter(result =>
result.status === 'fulfilled' && result.value.success
);
console.log('๐ OTP/DM: Publishing summary:');
console.log(` - Total relays attempted: ${relays.length}`);
console.log(` - NIP-17 successful publishes: ${successfulResults.length}/${relays.length}`);
console.log(` - Overall success rate: ${Math.round((successfulResults.length / relays.length) * 100)}%`);
if (successfulResults.length > 0) {
console.log('โ
OTP/DM: NIP-17 DM sent successfully to at least one relay');
console.log('๐ฑ OTP/DM: Check your Nostr client for the encrypted DM');
console.log('๐ OTP/DM: Message uses modern NIP-44 encryption with NIP-17 gift wrapping');
console.log('๐ฏ OTP/DM: This should work with all modern Nostr clients and relays');
} else {
console.error('โ OTP/DM: Failed to send DM to any relays');
console.log('๐ก OTP/DM: This might indicate relay connectivity issues or client compatibility problems');
}
// Clean up the pool safely
try {
console.log('๐งน OTP/DM: Cleaning up relay connections...');
pool.close(relays);
console.log('๐งน OTP/DM: Pool cleanup completed');
} catch (cleanupError) {
console.warn('โ ๏ธ OTP/DM: Pool cleanup warning (non-critical):', cleanupError.message);
}
// For demo purposes, also show the code in console
setTimeout(() => {
console.log(`๐ OTP/DM: Demo - Login code sent was: ${code}`);
console.log('๐ก OTP/DM: Check your Nostr DMs or use the code above for testing');
}, 2000);
return successfulResults.length > 0;
} catch (error) {
console.error('๐ฅ OTP/DM: NIP-17 implementation error:', error);
console.log('๐ก OTP/DM: This might indicate a problem with the nostr-tools NIP-17 implementation');
console.log(`๐ OTP/DM: Fallback - Login code for testing: ${code}`);
return false;
}
}
async _sendLegacyNip44Dm(senderSecretKey, recipientPubkey, dmContent, code) {
console.log('๐ OTP/DM: Using legacy NIP-44 direct message format');
try {
// Encrypt with NIP-44
const conversationKey = window.NostrTools.nip44.getConversationKey(senderSecretKey, recipientPubkey);
const nip44Content = window.NostrTools.nip44.encrypt(dmContent, conversationKey);
console.log('๐ OTP/DM: NIP-44 encrypted length:', nip44Content.length, 'chars');
// Create NIP-44 DM event (modern approach - no custom tags)
const nip44Event = {
kind: 4,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', recipientPubkey]
],
content: nip44Content
};
// Sign the event
const signedEvent = window.NostrTools.finalizeEvent(nip44Event, senderSecretKey);
console.log('๐ OTP/DM: NIP-44 signed event:', JSON.stringify(signedEvent, null, 2));
// Get relays to send to
const relays = this.options.relays || [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://relay.laantungir.net',
'wss://nostr.mom'
];
// Create a pool and send
const pool = new window.NostrTools.nip46.SimplePool();
const publishPromises = relays.map(async (relay, index) => {
console.log(`๐ก OTP/DM: Legacy NIP-44 [${index + 1}/${relays.length}] Connecting to relay ${relay}...`);
try {
const results = await pool.publish([relay], signedEvent);
console.log(`๐ก OTP/DM: Legacy NIP-44 Raw JSON response from ${relay}:`, JSON.stringify(results, null, 2));
const success = results.length > 0 && results[0].status === 'fulfilled';
if (!success && results.length > 0 && results[0].status === 'rejected') {
console.warn(`โ ๏ธ OTP/DM: Legacy NIP-44 rejected by ${relay}:`, results[0].reason);
}
return { relay, success, results, type: 'legacy-nip44' };
} catch (error) {
console.error(`โ OTP/DM: Legacy NIP-44 Failed to publish to ${relay}:`, error);
return { relay, success: false, error: error.message, type: 'legacy-nip44' };
}
});
const results = await Promise.allSettled(publishPromises);
const successfulResults = results.filter(result =>
result.status === 'fulfilled' && result.value.success
);
console.log('๐ OTP/DM: Legacy publishing summary:');
console.log(` - Legacy NIP-44 successful publishes: ${successfulResults.length}/${relays.length}`);
pool.close(relays);
return successfulResults.length > 0;
} catch (error) {
console.error('๐ฅ OTP/DM: Legacy NIP-44 error:', error);
console.log(`๐ OTP/DM: Fallback - Login code for testing: ${code}`);
return false;
}
}
_handleOtpVerification(pubkey, enteredCode) {
if (!enteredCode || enteredCode.length !== 6) {
this._showError('Please enter a 6-digit code');
return;
}
const storedCode = sessionStorage.getItem(`otp_${pubkey}`);
const timestamp = parseInt(sessionStorage.getItem(`otp_${pubkey}_timestamp`) || '0');
// Check if code is expired (5 minutes)
if (Date.now() - timestamp > 5 * 60 * 1000) {
sessionStorage.removeItem(`otp_${pubkey}`);
sessionStorage.removeItem(`otp_${pubkey}_timestamp`);
this._showError('Login code has expired. Please request a new one.');
return;
}
if (enteredCode === storedCode) {
// Clean up stored code
sessionStorage.removeItem(`otp_${pubkey}`);
sessionStorage.removeItem(`otp_${pubkey}_timestamp`);
console.log('OTP/DM: Code verified successfully');
// Set as read-only authentication (since we don't have the private key)
this._setAuthMethod('readonly', { pubkey });
} else {
this._showError('Invalid login code. Please check and try again.');
}
}
_getButtonStyle(type = 'primary') {
const baseStyle = `
display: block;
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
`;
if (type === 'primary') {
return baseStyle + `
background: #3b82f6;
color: white;
`;
} else {
return baseStyle + `
background: #6b7280;
color: white;
`;
}
}
// Public API
static init(options) {
if (Modal.instance) return Modal.instance;
Modal.instance = new Modal(options);
return Modal.instance;
}
static getInstance() {
return Modal.instance;
}
}
// Initialize global instance
let modalInstance = null;
window.addEventListener('load', () => {
modalInstance = new Modal();
});
// ======================================
// 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;
}
async init(options = {}) {
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
this.options = {
theme: 'light',
darkMode: false,
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
methods: {
extension: true,
local: true,
readonly: true,
connect: false,
otp: false
},
...options
};
// Set up window.nostr facade if no extension detected
if (this.extensionBridge.getExtensionCount() === 0) {
this._setupWindowNostrFacade();
}
this.initialized = true;
console.log('NOSTR_LOGIN_LITE: Initialization complete');
// Set up event listeners for authentication flow
this._setupAuthEventHandlers();
return this;
}
_setupWindowNostrFacade() {
if (typeof window !== 'undefined' && !window.nostr) {
window.nostr = new WindowNostr(this);
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed');
}
}
_setupAuthEventHandlers() {
if (typeof window === 'undefined') return;
// Listen for authentication method selection
window.addEventListener('nlMethodSelected', async (event) => {
console.log('NOSTR_LOGIN_LITE: Authentication method selected:', event.detail);
const { method, pubkey, signer, extension, secret } = event.detail;
try {
// Complete the authentication flow
await this._completeAuthentication(method, event.detail);
} catch (error) {
console.error('NOSTR_LOGIN_LITE: Authentication completion failed:', error);
this._dispatchAuthEvent('nlAuthFailed', { error: error.message });
}
});
}
async _completeAuthentication(method, authData) {
console.log('NOSTR_LOGIN_LITE: Completing authentication for method:', method);
const authResult = {
method,
pubkey: authData.pubkey,
timestamp: Date.now()
};
// Add method-specific data
switch (method) {
case 'extension':
authResult.extension = authData.extension;
break;
case 'local':
authResult.secret = authData.secret;
break;
case 'nip46':
authResult.signer = authData.signer;
authResult.remotePubkey = authData.signer.remotePubkey;
break;
case 'readonly':
// No additional data needed
break;
}
// Store authentication state
if (typeof localStorage !== 'undefined') {
localStorage.setItem('nl_current', JSON.stringify({
method,
pubkey: authData.pubkey,
timestamp: Date.now()
}));
}
console.log('NOSTR_LOGIN_LITE: Authentication completed successfully');
console.log('NOSTR_LOGIN_LITE: User pubkey:', authData.pubkey);
// Fetch and display profile information
await this._fetchAndDisplayProfile(authData.pubkey);
// Dispatch success event
this._dispatchAuthEvent('nlAuth', authResult);
}
async _fetchAndDisplayProfile(pubkey) {
console.log('NOSTR_LOGIN_LITE: Fetching profile for pubkey:', pubkey);
try {
// Create a simple pool for fetching profile
const pool = new window.NostrTools.SimplePool();
const relays = this.options.relays || ['wss://relay.damus.io', 'wss://nos.lol'];
console.log('NOSTR_LOGIN_LITE: Querying relays for profile:', relays);
// Fetch profile metadata (kind 0)
const profileEvents = await pool.querySync(relays, {
kinds: [0],
authors: [pubkey],
limit: 1
});
pool.close(relays);
if (profileEvents && profileEvents.length > 0) {
const profileEvent = profileEvents[0];
const profile = JSON.parse(profileEvent.content);
console.log('NOSTR_LOGIN_LITE: Profile fetched successfully:', profile);
// Display profile information
this._displayProfileInfo(pubkey, profile);
} else {
console.log('NOSTR_LOGIN_LITE: No profile found, displaying pubkey only');
this._displayProfileInfo(pubkey, null);
}
} catch (error) {
console.error('NOSTR_LOGIN_LITE: Failed to fetch profile:', error);
this._displayProfileInfo(pubkey, null);
}
}
_displayProfileInfo(pubkey, profile) {
console.log('NOSTR_LOGIN_LITE: Displaying profile info');
// Create or update profile display
let profileDiv = document.getElementById('nl-profile-info');
if (!profileDiv) {
profileDiv = document.createElement('div');
profileDiv.id = 'nl-profile-info';
profileDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
z-index: 9999;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(profileDiv);
}
const npub = window.NostrTools.nip19.npubEncode(pubkey);
const shortPubkey = `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
profileDiv.innerHTML = `