Files
nostr_login_lite/lite/build.js
2025-09-22 15:37:53 -04:00

2306 lines
81 KiB
JavaScript

/**
* 🏗️ NOSTR_LOGIN_LITE Build Script
*
* ⚠️ IMPORTANT: This file contains the source code for the NOSTR_LOGIN_LITE library!
* ⚠️ DO NOT edit lite/nostr-lite.js directly - it's auto-generated by this script!
* ⚠️ To modify the library, edit this file (build.js) and run: node build.js
*
* This script builds the two-file architecture:
* 1. nostr.bundle.js (official nostr-tools bundle - static file)
* 2. nostr-lite.js (NOSTR_LOGIN_LITE library - built by this script)
*
* Features included:
* - CSS-Only Theme System (no JSON duplication)
* - Modal UI Component
* - FloatingTab Component
* - Extension Bridge
* - Window.nostr facade
* - Main NostrLite class with all functionality
*/
const fs = require('fs');
const path = require('path');
function createNostrLoginLiteBundle() {
console.log('🔧 Creating NOSTR_LOGIN_LITE bundle for two-file architecture...');
const outputPath = path.join(__dirname, 'nostr-lite.js');
// Remove old bundle
try {
if (fs.existsSync(outputPath)) {
fs.unlinkSync(outputPath);
}
} catch (e) {
console.log('No old bundle to remove');
}
// Start with the bundle header
let bundle = `/**
* NOSTR_LOGIN_LITE - Authentication Library
*
* ⚠️ WARNING: THIS FILE IS AUTO-GENERATED - DO NOT EDIT MANUALLY!
* ⚠️ To make changes, edit lite/build.js and run: cd lite && node build.js
* ⚠️ Any manual edits to this file will be OVERWRITTEN when build.js runs!
*
* Two-file architecture:
* 1. Load nostr.bundle.js (official nostr-tools bundle)
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
* Generated on: ${new Date().toISOString()}
*/
// Verify dependencies are loaded
if (typeof window !== 'undefined') {
if (!window.NostrTools) {
console.error('NOSTR_LOGIN_LITE: nostr.bundle.js must be loaded first');
throw new Error('Missing dependency: nostr.bundle.js');
}
console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓');
console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools));
console.log('NOSTR_LOGIN_LITE: NIP-06 available:', !!window.NostrTools.nip06);
console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46);
}
// ======================================
// NOSTR_LOGIN_LITE Components
// ======================================
`;
// Embed CSS themes
console.log('🎨 Adding CSS-Only Theme System...');
const defaultThemeCssPath = path.join(__dirname, '../themes/default/theme.css');
const darkThemeCssPath = path.join(__dirname, '../themes/dark/theme.css');
if (fs.existsSync(defaultThemeCssPath) && fs.existsSync(darkThemeCssPath)) {
const defaultThemeCss = fs.readFileSync(defaultThemeCssPath, 'utf8')
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\${/g, '\\${');
const darkThemeCss = fs.readFileSync(darkThemeCssPath, 'utf8')
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\${/g, '\\${');
bundle += `// ======================================\n`;
bundle += `// CSS-Only Theme System\n`;
bundle += `// ======================================\n\n`;
bundle += `const THEME_CSS = {\n`;
bundle += ` 'default': \`${defaultThemeCss}\`,\n`;
bundle += ` 'dark': \`${darkThemeCss}\`\n`;
bundle += `};\n\n`;
bundle += `// Theme management functions\n`;
bundle += `function injectThemeCSS(themeName = 'default') {\n`;
bundle += ` if (typeof document !== 'undefined') {\n`;
bundle += ` // Remove existing theme CSS\n`;
bundle += ` const existingStyle = document.getElementById('nl-theme-css');\n`;
bundle += ` if (existingStyle) {\n`;
bundle += ` existingStyle.remove();\n`;
bundle += ` }\n`;
bundle += ` \n`;
bundle += ` // Inject selected theme CSS\n`;
bundle += ` const themeCss = THEME_CSS[themeName] || THEME_CSS['default'];\n`;
bundle += ` const style = document.createElement('style');\n`;
bundle += ` style.id = 'nl-theme-css';\n`;
bundle += ` style.textContent = themeCss;\n`;
bundle += ` document.head.appendChild(style);\n`;
bundle += ` console.log('NOSTR_LOGIN_LITE: ' + themeName + ' theme CSS injected');\n`;
bundle += ` }\n`;
bundle += `}\n\n`;
// Auto-inject default theme on load
bundle += `// Auto-inject default theme when DOM is ready\n`;
bundle += `if (typeof document !== 'undefined') {\n`;
bundle += ` if (document.readyState === 'loading') {\n`;
bundle += ` document.addEventListener('DOMContentLoaded', () => injectThemeCSS('default'));\n`;
bundle += ` } else {\n`;
bundle += ` injectThemeCSS('default');\n`;
bundle += ` }\n`;
bundle += `}\n\n`;
}
// Add Modal UI
const modalPath = path.join(__dirname, 'ui/modal.js');
if (fs.existsSync(modalPath)) {
console.log('📄 Adding Modal UI...');
let modalContent = fs.readFileSync(modalPath, 'utf8');
// Read version from VERSION file for bottom-right display
const versionPath = path.join(__dirname, 'VERSION');
let versionString = '';
if (fs.existsSync(versionPath)) {
try {
const version = fs.readFileSync(versionPath, 'utf8').trim();
versionString = 'v' + version;
console.log('🔢 Using version: ' + version);
} catch (error) {
console.warn('⚠️ Could not read VERSION file, no version will be displayed');
}
} else {
console.log('📋 No VERSION file found, no version will be displayed');
}
// Keep modal title as just "Nostr Login" (no version injection)
// Add version element in bottom-right corner if version exists
if (versionString) {
// Find the modalContent.appendChild(this.modalBody); line and insert version element before it
modalContent = modalContent.replace(
/modalContent\.appendChild\(this\.modalBody\);/,
`// Add version element in bottom-right corner aligned with modal body
const versionElement = document.createElement('div');
versionElement.textContent = '${versionString}';
versionElement.style.cssText = \`
position: absolute;
bottom: 8px;
right: 24px;
font-size: 14px;
color: #666666;
font-family: var(--nl-font-family, 'Courier New', monospace);
pointer-events: none;
z-index: 1;
\`;
modalContent.appendChild(versionElement);
modalContent.appendChild(this.modalBody);`
);
}
// Skip header comments
let lines = modalContent.split('\n');
let contentStartIndex = 0;
for (let i = 0; i < Math.min(15, lines.length); i++) {
const line = lines[i].trim();
if (line.startsWith('/**') || line.startsWith('*') ||
line.startsWith('/*') || line.startsWith('//')) {
contentStartIndex = i + 1;
} else if (line && !line.startsWith('*') && !line.startsWith('//')) {
break;
}
}
if (contentStartIndex > 0) {
lines = lines.slice(contentStartIndex);
}
bundle += `// ======================================\n`;
bundle += `// Modal UI Component\n`;
bundle += `// ======================================\n\n`;
bundle += lines.join('\n');
bundle += '\n\n';
} else {
console.warn('⚠️ Modal UI not found: ui/modal.js');
}
// Add main library code
console.log('📄 Adding Main Library...');
bundle += `
// ======================================
// FloatingTab Component (Recovered from git history)
// ======================================
class FloatingTab {
constructor(modal, options = {}) {
this.modal = modal;
this.options = {
enabled: true,
hPosition: 1.0, // 0.0 = left, 1.0 = right
vPosition: 0.5, // 0.0 = top, 1.0 = bottom
offset: { x: 0, y: 0 },
appearance: {
style: 'pill', // 'pill', 'square', 'circle'
theme: 'auto', // 'auto', 'light', 'dark'
icon: '',
text: 'Login',
iconOnly: false
},
behavior: {
hideWhenAuthenticated: true,
showUserInfo: true,
autoSlide: true,
persistent: false
},
getUserInfo: false,
getUserRelay: [],
...options
};
this.userProfile = null;
this.container = null;
this.isVisible = false;
if (this.options.enabled) {
this._init();
}
}
_init() {
console.log('FloatingTab: Initializing with options:', this.options);
this._createContainer();
this._setupEventListeners();
this._updateAppearance();
this._position();
this.show();
}
// Get authentication state from authoritative source (Global Storage-Based Function)
_getAuthState() {
return window.NOSTR_LOGIN_LITE?.getAuthState?.() || null;
}
_createContainer() {
// Remove existing floating tab if any
const existingTab = document.getElementById('nl-floating-tab');
if (existingTab) {
existingTab.remove();
}
this.container = document.createElement('div');
this.container.id = 'nl-floating-tab';
this.container.className = 'nl-floating-tab';
// Base styles - positioning and behavior
this.container.style.cssText = \`
position: fixed;
z-index: 9999;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 500;
padding: 8px 16px;
min-width: 80px;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
\`;
document.body.appendChild(this.container);
}
_setupEventListeners() {
if (!this.container) return;
// Click handler
this.container.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this._handleClick();
});
// Hover effects
this.container.addEventListener('mouseenter', () => {
if (this.options.behavior.autoSlide) {
this._slideIn();
}
});
this.container.addEventListener('mouseleave', () => {
if (this.options.behavior.autoSlide) {
this._slideOut();
}
});
// Listen for authentication events
window.addEventListener('nlMethodSelected', (e) => {
console.log('🔍 FloatingTab: Authentication method selected event received');
console.log('🔍 FloatingTab: Event detail:', e.detail);
this._handleAuth(e.detail);
});
window.addEventListener('nlAuthRestored', (e) => {
console.log('🔍 FloatingTab: ✅ Authentication restored event received');
console.log('🔍 FloatingTab: Event detail:', e.detail);
console.log('🔍 FloatingTab: Calling _handleAuth with restored data...');
this._handleAuth(e.detail);
});
window.addEventListener('nlLogout', () => {
console.log('🔍 FloatingTab: Logout event received');
this._handleLogout();
});
// Check for existing authentication state on initialization
window.addEventListener('load', () => {
setTimeout(() => {
this._checkExistingAuth();
}, 1000); // Wait 1 second for all initialization to complete
});
}
// Check for existing authentication on page load
async _checkExistingAuth() {
console.log('🔍 FloatingTab: === _checkExistingAuth START ===');
try {
const storageKey = 'nostr_login_lite_auth';
let storedAuth = null;
// Try sessionStorage first, then localStorage
if (sessionStorage.getItem(storageKey)) {
storedAuth = JSON.parse(sessionStorage.getItem(storageKey));
console.log('🔍 FloatingTab: Found auth in sessionStorage:', storedAuth.method);
} else if (localStorage.getItem(storageKey)) {
storedAuth = JSON.parse(localStorage.getItem(storageKey));
console.log('🔍 FloatingTab: Found auth in localStorage:', storedAuth.method);
}
if (storedAuth) {
// Check if stored auth is not expired
const maxAge = storedAuth.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
if (Date.now() - storedAuth.timestamp <= maxAge) {
console.log('🔍 FloatingTab: Found valid stored auth, simulating auth event');
// Create auth data object for FloatingTab
const authData = {
method: storedAuth.method,
pubkey: storedAuth.pubkey
};
// For extensions, try to find the extension
if (storedAuth.method === 'extension') {
if (window.nostr && window.nostr.constructor?.name !== 'WindowNostr') {
authData.extension = window.nostr;
}
}
await this._handleAuth(authData);
} else {
console.log('🔍 FloatingTab: Stored auth expired, clearing');
sessionStorage.removeItem(storageKey);
localStorage.removeItem(storageKey);
}
} else {
console.log('🔍 FloatingTab: No existing authentication found');
}
} catch (error) {
console.error('🔍 FloatingTab: Error checking existing auth:', error);
}
console.log('🔍 FloatingTab: === _checkExistingAuth END ===');
}
_handleClick() {
console.log('FloatingTab: Clicked');
const authState = this._getAuthState();
if (authState && this.options.behavior.showUserInfo) {
// Show user menu or profile options
this._showUserMenu();
} else {
// Always open login modal (consistent with login buttons)
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
}
// Check if object is a real extension (same logic as NostrLite._isRealExtension)
_isRealExtension(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
// Must have required Nostr methods
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
return false;
}
// Exclude our own library classes
const constructorName = obj.constructor?.name;
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
return false;
}
// Exclude NostrTools library object
if (obj === window.NostrTools) {
return false;
}
// Conservative check: Look for common extension characteristics
const extensionIndicators = [
'_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope',
'_requests', '_pubkey', 'name', 'version', 'description'
];
const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop));
// Additional check: Extensions often have specific constructor patterns
const hasExtensionConstructor = constructorName &&
constructorName !== 'Object' &&
constructorName !== 'Function';
return hasIndicators || hasExtensionConstructor;
}
// Try to login with extension and trigger proper persistence
async _tryExtensionLogin(extension) {
try {
console.log('FloatingTab: Attempting extension login');
// Get pubkey from extension
const pubkey = await extension.getPublicKey();
console.log('FloatingTab: Extension provided pubkey:', pubkey);
// Create extension auth data
const extensionAuth = {
method: 'extension',
pubkey: pubkey,
extension: extension
};
// **CRITICAL FIX**: Dispatch nlMethodSelected event to trigger persistence
console.log('FloatingTab: Dispatching nlMethodSelected for persistence');
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('nlMethodSelected', {
detail: extensionAuth
}));
}
// Also call our local _handleAuth for UI updates
await this._handleAuth(extensionAuth);
} catch (error) {
console.error('FloatingTab: Extension login failed:', error);
// Fall back to opening modal
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
}
async _handleAuth(authData) {
console.log('🔍 FloatingTab: === _handleAuth START ===');
console.log('🔍 FloatingTab: authData received:', authData);
// Wait a brief moment for WindowNostr to process the authentication
setTimeout(async () => {
console.log('🔍 FloatingTab: Checking authentication state from authoritative source...');
const authState = this._getAuthState();
const isAuthenticated = !!authState;
console.log('🔍 FloatingTab: Authoritative auth state:', authState);
console.log('🔍 FloatingTab: Is authenticated:', isAuthenticated);
if (isAuthenticated) {
console.log('🔍 FloatingTab: ✅ Authentication verified from authoritative source');
} else {
console.error('🔍 FloatingTab: ❌ Authentication not found in authoritative source');
}
// Fetch user profile if enabled and we have a pubkey
if (this.options.getUserInfo && authData.pubkey) {
console.log('🔍 FloatingTab: getUserInfo enabled, fetching profile for:', authData.pubkey);
try {
const profile = await this._fetchUserProfile(authData.pubkey);
this.userProfile = profile;
console.log('🔍 FloatingTab: User profile fetched:', profile);
} catch (error) {
console.warn('🔍 FloatingTab: Failed to fetch user profile:', error);
this.userProfile = null;
}
} else {
console.log('🔍 FloatingTab: getUserInfo disabled or no pubkey, skipping profile fetch');
}
this._updateAppearance(); // Update UI based on authoritative state
console.log('🔍 FloatingTab: hideWhenAuthenticated option:', this.options.behavior.hideWhenAuthenticated);
if (this.options.behavior.hideWhenAuthenticated && isAuthenticated) {
console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true and authenticated)');
this.hide();
} else {
console.log('🔍 FloatingTab: Keeping tab visible');
}
}, 500); // Wait 500ms for WindowNostr to complete authentication processing
console.log('🔍 FloatingTab: === _handleAuth END ===');
}
_handleLogout() {
console.log('FloatingTab: Handling logout');
this.userProfile = null;
if (this.options.behavior.hideWhenAuthenticated) {
this.show();
}
this._updateAppearance();
}
_showUserMenu() {
// Simple user menu - could be expanded
const menu = document.createElement('div');
menu.style.cssText = \`
position: fixed;
background: var(--nl-secondary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
padding: 12px;
z-index: 10000;
font-family: var(--nl-font-family);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
\`;
// Position near the floating tab
const tabRect = this.container.getBoundingClientRect();
if (this.options.hPosition > 0.5) {
// Tab is on right side, show menu to the left
menu.style.right = (window.innerWidth - tabRect.left) + 'px';
} else {
// Tab is on left side, show menu to the right
menu.style.left = tabRect.right + 'px';
}
menu.style.top = tabRect.top + 'px';
// Menu content - use _getAuthState() as single source of truth
const authState = this._getAuthState();
let userDisplay;
if (authState?.pubkey) {
// Use profile name if available, otherwise pubkey
if (this.userProfile?.name || this.userProfile?.display_name) {
const userName = this.userProfile.name || this.userProfile.display_name;
userDisplay = userName.length > 16 ? userName.slice(0, 16) + '...' : userName;
} else {
userDisplay = authState.pubkey.slice(0, 8) + '...' + authState.pubkey.slice(-4);
}
} else {
userDisplay = 'Authenticated';
}
menu.innerHTML = \`
<div style="margin-bottom: 8px; font-weight: bold; color: var(--nl-primary-color);">\${userDisplay}</div>
<button onclick="window.NOSTR_LOGIN_LITE.logout(); this.parentElement.remove();"
style="background: var(--nl-secondary-color); color: var(--nl-primary-color);
border: 1px solid var(--nl-primary-color); border-radius: 4px;
padding: 6px 12px; cursor: pointer; width: 100%;">
Logout
</button>
\`;
document.body.appendChild(menu);
// Auto-remove menu after delay or on outside click
const removeMenu = () => menu.remove();
setTimeout(removeMenu, 5000);
document.addEventListener('click', function onOutsideClick(e) {
if (!menu.contains(e.target) && e.target !== this.container) {
removeMenu();
document.removeEventListener('click', onOutsideClick);
}
});
}
_updateAppearance() {
if (!this.container) return;
// Query authoritative source for all state information
const authState = this._getAuthState();
const isAuthenticated = authState !== null;
// Update content
if (isAuthenticated && this.options.behavior.showUserInfo) {
let display;
// Use profile name if available, otherwise fall back to pubkey
if (this.userProfile?.name || this.userProfile?.display_name) {
const userName = this.userProfile.name || this.userProfile.display_name;
display = this.options.appearance.iconOnly
? userName.slice(0, 8)
: userName;
} else if (authState?.pubkey) {
// Fallback to pubkey display
display = this.options.appearance.iconOnly
? authState.pubkey.slice(0, 6)
: authState.pubkey.slice(0, 6) + '...';
} else {
display = this.options.appearance.iconOnly ? 'User' : 'Authenticated';
}
this.container.textContent = display;
this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
} else {
const display = this.options.appearance.iconOnly ?
this.options.appearance.icon :
(this.options.appearance.icon ? this.options.appearance.icon + ' ' + this.options.appearance.text : this.options.appearance.text);
this.container.textContent = display;
this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
}
// Apply appearance styles based on current state
this._applyThemeStyles();
}
_applyThemeStyles() {
if (!this.container) return;
// The CSS classes will handle the theming through CSS custom properties
// Additional style customizations can be added here if needed
// Apply style variant
if (this.options.appearance.style === 'circle') {
this.container.style.borderRadius = '50%';
this.container.style.width = '48px';
this.container.style.height = '48px';
this.container.style.minWidth = '48px';
this.container.style.padding = '0';
} else if (this.options.appearance.style === 'square') {
this.container.style.borderRadius = '4px';
} else {
// pill style (default)
this.container.style.borderRadius = 'var(--nl-border-radius)';
}
}
async _fetchUserProfile(pubkey) {
if (!this.options.getUserInfo) {
console.log('FloatingTab: getUserInfo disabled, skipping profile fetch');
return null;
}
// Determine which relays to use
const relays = this.options.getUserRelay.length > 0
? this.options.getUserRelay
: ['wss://relay.damus.io', 'wss://nos.lol'];
console.log('FloatingTab: Fetching profile from relays:', relays);
try {
// Create a SimplePool instance for querying
const pool = new window.NostrTools.SimplePool();
// Query for kind 0 (user metadata) events
const events = await pool.querySync(relays, {
kinds: [0],
authors: [pubkey],
limit: 1
}, { timeout: 5000 });
console.log('FloatingTab: Profile query returned', events.length, 'events');
if (events.length === 0) {
console.log('FloatingTab: No profile events found');
return null;
}
// Get the most recent event
const latestEvent = events.sort((a, b) => b.created_at - a.created_at)[0];
try {
const profile = JSON.parse(latestEvent.content);
console.log('FloatingTab: Parsed profile:', profile);
// Find the best name from any key containing "name" (case-insensitive)
let bestName = null;
const nameKeys = Object.keys(profile).filter(key =>
key.toLowerCase().includes('name') &&
typeof profile[key] === 'string' &&
profile[key].trim().length > 0
);
if (nameKeys.length > 0) {
// Find the shortest name value
bestName = nameKeys
.map(key => profile[key].trim())
.reduce((shortest, current) =>
current.length < shortest.length ? current : shortest
);
console.log('FloatingTab: Found name keys:', nameKeys, 'selected:', bestName);
}
// Return relevant profile fields with the best name
return {
name: bestName,
display_name: profile.display_name || null,
about: profile.about || null,
picture: profile.picture || null,
nip05: profile.nip05 || null
};
} catch (parseError) {
console.warn('FloatingTab: Failed to parse profile JSON:', parseError);
return null;
}
} catch (error) {
console.error('FloatingTab: Profile fetch error:', error);
return null;
}
}
_position() {
if (!this.container) return;
const padding = 16; // Distance from screen edge
// Calculate position based on percentage
const x = this.options.hPosition * (window.innerWidth - this.container.offsetWidth - padding * 2) + padding + this.options.offset.x;
const y = this.options.vPosition * (window.innerHeight - this.container.offsetHeight - padding * 2) + padding + this.options.offset.y;
this.container.style.left = x + 'px';
this.container.style.top = y + 'px';
console.log('FloatingTab: Positioned at (' + x + ', ' + y + ')');
}
_slideIn() {
if (!this.container || !this.options.behavior.autoSlide) return;
// Slide towards center slightly
const currentTransform = this.container.style.transform || '';
if (this.options.hPosition > 0.5) {
this.container.style.transform = currentTransform + ' translateX(-8px)';
} else {
this.container.style.transform = currentTransform + ' translateX(8px)';
}
}
_slideOut() {
if (!this.container || !this.options.behavior.autoSlide) return;
// Reset position
this.container.style.transform = '';
}
show() {
if (!this.container) return;
this.container.style.display = 'flex';
this.isVisible = true;
console.log('FloatingTab: Shown');
}
hide() {
if (!this.container) return;
this.container.style.display = 'none';
this.isVisible = false;
console.log('FloatingTab: Hidden');
}
destroy() {
if (this.container) {
this.container.remove();
this.container = null;
}
this.isVisible = false;
console.log('FloatingTab: Destroyed');
}
// Update options and re-apply
updateOptions(newOptions) {
this.options = { ...this.options, ...newOptions };
if (this.container) {
this._updateAppearance();
this._position();
}
}
// Get current state
getState() {
const authState = this._getAuthState();
return {
isVisible: this.isVisible,
isAuthenticated: !!authState,
userInfo: authState,
options: this.options
};
}
}
// ======================================
// Main NOSTR_LOGIN_LITE Library
// ======================================
// Extension Bridge for managing browser extensions
class ExtensionBridge {
constructor() {
this.extensions = new Map();
this.primaryExtension = null;
this._detectExtensions();
}
_detectExtensions() {
// Common extension locations
const locations = [
{ path: 'window.nostr', name: 'Generic' },
{ path: 'window.alby?.nostr', name: 'Alby' },
{ path: 'window.nos2x?.nostr', name: 'nos2x' },
{ path: 'window.flamingo?.nostr', name: 'Flamingo' },
{ path: 'window.getAlby?.nostr', name: 'Alby Legacy' },
{ path: 'window.mutiny?.nostr', name: 'Mutiny' }
];
for (const location of locations) {
try {
const obj = eval(location.path);
if (obj && typeof obj.getPublicKey === 'function') {
this.extensions.set(location.name, {
name: location.name,
extension: obj,
constructor: obj.constructor?.name || 'Unknown'
});
if (!this.primaryExtension) {
this.primaryExtension = this.extensions.get(location.name);
}
}
} catch (e) {
// Extension not available
}
}
}
getAllExtensions() {
return Array.from(this.extensions.values());
}
getExtensionCount() {
return this.extensions.size;
}
}
// Main NostrLite class
class NostrLite {
constructor() {
this.options = {};
this.extensionBridge = new ExtensionBridge();
this.initialized = false;
this.currentTheme = 'default';
this.modal = null;
this.floatingTab = null;
}
async init(options = {}) {
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
this.options = {
theme: 'default',
persistence: true, // Enable persistent authentication by default
isolateSession: false, // Use localStorage by default for cross-window persistence
methods: {
extension: true,
local: true,
seedphrase: false,
readonly: true,
connect: false,
otp: false
},
floatingTab: {
enabled: false,
hPosition: 1.0,
vPosition: 0.5,
offset: { x: 0, y: 0 },
appearance: {
style: 'pill',
theme: 'auto',
icon: '',
text: 'Login',
iconOnly: false
},
behavior: {
hideWhenAuthenticated: true,
showUserInfo: true,
autoSlide: true,
persistent: false
},
getUserInfo: false,
getUserRelay: []
},
...options
};
// Apply the selected theme (CSS-only)
this.switchTheme(this.options.theme);
// Always set up window.nostr facade to handle multiple extensions properly
console.log('🔍 NOSTR_LOGIN_LITE: Setting up facade before other initialization...');
await this._setupWindowNostrFacade();
console.log('🔍 NOSTR_LOGIN_LITE: Facade setup complete, continuing initialization...');
// Create modal during init (matching original git architecture)
this.modal = new Modal(this.options);
console.log('NOSTR_LOGIN_LITE: Modal created during init');
// Initialize floating tab if enabled
if (this.options.floatingTab.enabled) {
this.floatingTab = new FloatingTab(this.modal, this.options.floatingTab);
console.log('NOSTR_LOGIN_LITE: Floating tab initialized');
}
// Attempt to restore authentication state if persistence is enabled (AFTER facade is ready)
if (this.options.persistence) {
console.log('🔍 NOSTR_LOGIN_LITE: Persistence enabled, attempting auth restoration...');
await this._attemptAuthRestore();
} else {
console.log('🔍 NOSTR_LOGIN_LITE: Persistence disabled in options');
}
this.initialized = true;
console.log('NOSTR_LOGIN_LITE: Initialization complete');
return this;
}
async _setupWindowNostrFacade() {
if (typeof window !== 'undefined') {
console.log('🔍 NOSTR_LOGIN_LITE: === EXTENSION-FIRST FACADE SETUP ===');
console.log('🔍 NOSTR_LOGIN_LITE: Current window.nostr:', window.nostr);
console.log('🔍 NOSTR_LOGIN_LITE: Constructor:', window.nostr?.constructor?.name);
// EXTENSION-FIRST ARCHITECTURE: Never interfere with real extensions
if (this._isRealExtension(window.nostr)) {
console.log('🔍 NOSTR_LOGIN_LITE: ✅ REAL EXTENSION DETECTED - WILL NOT INSTALL FACADE');
console.log('🔍 NOSTR_LOGIN_LITE: Extension constructor:', window.nostr.constructor?.name);
console.log('🔍 NOSTR_LOGIN_LITE: Extensions will handle window.nostr directly');
// Store reference for persistence verification
this.detectedExtension = window.nostr;
this.hasExtension = true;
this.facadeInstalled = false; // We deliberately don't install facade for extensions
console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - no facade interference');
return; // Don't install facade at all for extensions
}
// NO EXTENSION: Install facade for local/NIP-46/readonly methods
console.log('🔍 NOSTR_LOGIN_LITE: ❌ No real extension detected');
console.log('🔍 NOSTR_LOGIN_LITE: Installing facade for non-extension authentication');
this.hasExtension = false;
this._installFacade(window.nostr); // Install facade with any existing nostr object
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade installed for local/NIP-46/readonly methods');
// CRITICAL FIX: Immediately attempt to restore auth state after facade installation
if (this.facadeInstalled && window.nostr?.restoreAuthState) {
console.log('🔍 NOSTR_LOGIN_LITE: 🔄 IMMEDIATELY attempting auth restoration after facade installation');
try {
const restoredAuth = await window.nostr.restoreAuthState();
if (restoredAuth) {
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Auth state restored immediately during facade setup!');
console.log('🔍 NOSTR_LOGIN_LITE: Method:', restoredAuth.method);
console.log('🔍 NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey);
// Update facade's authState immediately
window.nostr.authState = restoredAuth;
} else {
console.log('🔍 NOSTR_LOGIN_LITE: ❌ No auth state to restore during facade setup');
}
} catch (error) {
console.error('🔍 NOSTR_LOGIN_LITE: ❌ Error restoring auth during facade setup:', error);
}
}
}
}
_installFacade(existingNostr = null, forceInstall = false) {
if (typeof window !== 'undefined' && (!this.facadeInstalled || forceInstall)) {
console.log('🔍 NOSTR_LOGIN_LITE: === _installFacade CALLED ===');
console.log('🔍 NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr);
console.log('🔍 NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name);
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr);
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name);
console.log('🔍 NOSTR_LOGIN_LITE: forceInstall flag:', forceInstall);
const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession });
window.nostr = facade;
this.facadeInstalled = true;
console.log('🔍 NOSTR_LOGIN_LITE: === FACADE INSTALLED FOR PERSISTENCE ===');
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr);
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name);
console.log('🔍 NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr);
} else if (typeof window !== 'undefined') {
console.log('🔍 NOSTR_LOGIN_LITE: _installFacade skipped - facadeInstalled:', this.facadeInstalled, 'forceInstall:', forceInstall);
}
}
// Conservative method to identify real browser extensions
_isRealExtension(obj) {
console.log('NOSTR_LOGIN_LITE: === _isRealExtension (Conservative) ===');
console.log('NOSTR_LOGIN_LITE: obj:', obj);
console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj);
if (!obj || typeof obj !== 'object') {
console.log('NOSTR_LOGIN_LITE: ✗ Not an object');
return false;
}
// Must have required Nostr methods
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
console.log('NOSTR_LOGIN_LITE: ✗ Missing required NIP-07 methods');
return false;
}
// Exclude our own library classes
const constructorName = obj.constructor?.name;
console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName);
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
console.log('NOSTR_LOGIN_LITE: ✗ Is our library class - NOT an extension');
return false;
}
// Exclude NostrTools library object
if (obj === window.NostrTools) {
console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object - NOT an extension');
return false;
}
// Conservative check: Look for common extension characteristics
// Real extensions usually have some of these internal properties
const extensionIndicators = [
'_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope',
'_requests', '_pubkey', 'name', 'version', 'description'
];
const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop));
// Additional check: Extensions often have specific constructor patterns
const hasExtensionConstructor = constructorName &&
constructorName !== 'Object' &&
constructorName !== 'Function';
const isExtension = hasIndicators || hasExtensionConstructor;
console.log('NOSTR_LOGIN_LITE: Extension indicators found:', hasIndicators);
console.log('NOSTR_LOGIN_LITE: Has extension constructor:', hasExtensionConstructor);
console.log('NOSTR_LOGIN_LITE: Final result for', constructorName, ':', isExtension);
return isExtension;
}
launch(startScreen = 'login') {
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
if (this.modal) {
this.modal.open({ startScreen });
} else {
console.error('NOSTR_LOGIN_LITE: Modal not initialized - call init() first');
}
}
// Attempt to restore authentication state
async _attemptAuthRestore() {
try {
console.log('🔍 NOSTR_LOGIN_LITE: === _attemptAuthRestore START ===');
console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension);
console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled);
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr:', window.nostr?.constructor?.name);
if (this.hasExtension) {
// EXTENSION MODE: Use custom extension persistence logic
console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - using extension-specific restore');
const restoredAuth = await this._attemptExtensionRestore();
if (restoredAuth) {
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth restored successfully!');
return restoredAuth;
} else {
console.log('🔍 NOSTR_LOGIN_LITE: ❌ Extension auth could not be restored');
return null;
}
} else if (this.facadeInstalled && window.nostr?.restoreAuthState) {
// NON-EXTENSION MODE: Use facade persistence logic
console.log('🔍 NOSTR_LOGIN_LITE: Non-extension mode - using facade restore');
const restoredAuth = await window.nostr.restoreAuthState();
if (restoredAuth) {
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade auth restored successfully!');
console.log('🔍 NOSTR_LOGIN_LITE: Method:', restoredAuth.method);
console.log('🔍 NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey);
// CRITICAL FIX: Activate facade resilience system for non-extension methods
// Extensions like nos2x can override our facade after page refresh
if (restoredAuth.method === 'local' || restoredAuth.method === 'nip46') {
console.log('🔍 NOSTR_LOGIN_LITE: 🛡️ Activating facade resilience system for page refresh');
this._activateResilienceProtection(restoredAuth.method);
}
// Handle NIP-46 reconnection requirement
if (restoredAuth.requiresReconnection) {
console.log('🔍 NOSTR_LOGIN_LITE: NIP-46 connection requires user reconnection');
this._showReconnectionPrompt(restoredAuth);
}
return restoredAuth;
} else {
console.log('🔍 NOSTR_LOGIN_LITE: ❌ Facade auth could not be restored');
return null;
}
} else {
console.log('🔍 NOSTR_LOGIN_LITE: ❌ No restoration method available');
console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension);
console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled);
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr.restoreAuthState:', typeof window.nostr?.restoreAuthState);
return null;
}
} catch (error) {
console.error('🔍 NOSTR_LOGIN_LITE: Auth restoration failed with error:', error);
console.error('🔍 NOSTR_LOGIN_LITE: Error stack:', error.stack);
return null;
}
}
// Activate facade resilience protection against extension overrides
_activateResilienceProtection(method) {
console.log('🛡️ NOSTR_LOGIN_LITE: === ACTIVATING RESILIENCE PROTECTION ===');
console.log('🛡️ NOSTR_LOGIN_LITE: Protecting facade for method:', method);
// Store the current extension if any (for potential restoration later)
const preservedExtension = this.preservedExtension ||
((window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null);
// DELAYED FACADE RESILIENCE - Reinstall after extension override attempts
const forceReinstallFacade = () => {
console.log('🛡️ NOSTR_LOGIN_LITE: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name);
// If facade was overridden by extension, reinstall it
if (window.nostr?.constructor?.name !== 'WindowNostr') {
console.log('🛡️ NOSTR_LOGIN_LITE: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method);
this._installFacade(preservedExtension, true);
console.log('🛡️ NOSTR_LOGIN_LITE: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name);
// Schedule another check in case of persistent extension override
setTimeout(() => {
if (window.nostr?.constructor?.name !== 'WindowNostr') {
console.log('🛡️ NOSTR_LOGIN_LITE: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method);
this._installFacade(preservedExtension, true);
}
}, 1000);
} else {
console.log('🛡️ NOSTR_LOGIN_LITE: Facade persistence verified - no override detected');
}
};
// Schedule resilience checks at multiple intervals (same as Modal)
setTimeout(forceReinstallFacade, 100); // Quick check
setTimeout(forceReinstallFacade, 500); // Main check
setTimeout(forceReinstallFacade, 1500); // Final check
console.log('🛡️ NOSTR_LOGIN_LITE: Resilience protection scheduled for method:', method);
}
// Extension-specific authentication restoration
async _attemptExtensionRestore() {
try {
console.log('🔍 NOSTR_LOGIN_LITE: === _attemptExtensionRestore START ===');
// Use a simple AuthManager instance for extension persistence
const authManager = new AuthManager({ isolateSession: this.options?.isolateSession });
const storedAuth = await authManager.restoreAuthState();
if (!storedAuth || storedAuth.method !== 'extension') {
console.log('🔍 NOSTR_LOGIN_LITE: No extension auth state stored');
return null;
}
// Verify the extension is still available and working
if (!window.nostr || !this._isRealExtension(window.nostr)) {
console.log('🔍 NOSTR_LOGIN_LITE: Extension no longer available');
authManager.clearAuthState(); // Clear invalid state
return null;
}
try {
// Test that the extension still works with the same pubkey
const currentPubkey = await window.nostr.getPublicKey();
if (currentPubkey !== storedAuth.pubkey) {
console.log('🔍 NOSTR_LOGIN_LITE: Extension pubkey changed, clearing state');
authManager.clearAuthState();
return null;
}
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth verification successful');
// Create extension auth data for UI restoration
const extensionAuth = {
method: 'extension',
pubkey: storedAuth.pubkey,
extension: window.nostr
};
// Dispatch restoration event so UI can update
if (typeof window !== 'undefined') {
console.log('🔍 NOSTR_LOGIN_LITE: Dispatching nlAuthRestored event for extension');
window.dispatchEvent(new CustomEvent('nlAuthRestored', {
detail: extensionAuth
}));
}
return extensionAuth;
} catch (error) {
console.log('🔍 NOSTR_LOGIN_LITE: Extension verification failed:', error);
authManager.clearAuthState(); // Clear invalid state
return null;
}
} catch (error) {
console.error('🔍 NOSTR_LOGIN_LITE: Extension restore failed:', error);
return null;
}
}
// Show prompt for NIP-46 reconnection
_showReconnectionPrompt(authData) {
console.log('NOSTR_LOGIN_LITE: Showing reconnection prompt for NIP-46');
// Dispatch event that UI can listen to
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('nlReconnectionRequired', {
detail: {
method: authData.method,
pubkey: authData.pubkey,
connectionData: authData.connectionData,
message: 'Your NIP-46 session has expired. Please reconnect to continue.'
}
}));
}
}
logout() {
console.log('NOSTR_LOGIN_LITE: Logout called');
// Clear legacy stored data
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('nl_current');
}
// Clear current authentication state directly from storage
// This works for ALL methods including extensions (fixes the bug)
clearAuthState();
// Dispatch logout event for UI updates
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('nlLogout', {
detail: { timestamp: Date.now() }
}));
}
}
// CSS-only theme switching
switchTheme(themeName) {
console.log('NOSTR_LOGIN_LITE: Switching to ' + themeName + ' theme');
if (THEME_CSS[themeName]) {
injectThemeCSS(themeName);
this.currentTheme = themeName;
// Dispatch theme change event
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('nlThemeChanged', {
detail: { theme: themeName }
}));
}
return { theme: themeName };
} else {
console.warn("Theme '" + themeName + "' not found, using default");
injectThemeCSS('default');
this.currentTheme = 'default';
return { theme: 'default' };
}
}
getCurrentTheme() {
return this.currentTheme;
}
getAvailableThemes() {
return Object.keys(THEME_CSS);
}
embed(container, options = {}) {
console.log('NOSTR_LOGIN_LITE: Creating embedded modal in container:', container);
const embedOptions = {
...this.options,
...options,
embedded: container
};
// Create new modal instance for embedding
const embeddedModal = new Modal(embedOptions);
embeddedModal.open();
return embeddedModal;
}
// Floating tab management methods
showFloatingTab() {
if (this.floatingTab) {
this.floatingTab.show();
} else {
console.warn('NOSTR_LOGIN_LITE: Floating tab not enabled');
}
}
hideFloatingTab() {
if (this.floatingTab) {
this.floatingTab.hide();
}
}
toggleFloatingTab() {
if (this.floatingTab) {
if (this.floatingTab.isVisible) {
this.floatingTab.hide();
} else {
this.floatingTab.show();
}
}
}
updateFloatingTab(options) {
if (this.floatingTab) {
this.floatingTab.updateOptions(options);
}
}
getFloatingTabState() {
return this.floatingTab ? this.floatingTab.getState() : null;
}
}
// ======================================
// Simplified Authentication Manager (Unified Plaintext Storage)
// ======================================
// Simple authentication state manager - plaintext storage for maximum usability
class AuthManager {
constructor(options = {}) {
this.storageKey = 'nostr_login_lite_auth';
this.currentAuthState = null;
// Configure storage type based on isolateSession option
if (options.isolateSession) {
this.storage = sessionStorage;
console.log('🔐 AuthManager: Using sessionStorage for per-window isolation');
} else {
this.storage = localStorage;
console.log('🔐 AuthManager: Using localStorage for cross-window persistence');
}
console.warn('🔐 SECURITY: Private keys stored unencrypted in browser storage');
console.warn('🔐 For production apps, implement your own secure storage');
}
// Save authentication state using unified plaintext approach
async saveAuthState(authData) {
try {
console.log('🔐 AuthManager: Saving auth state with plaintext storage');
console.warn('🔐 SECURITY: Private key will be stored unencrypted for maximum usability');
const authState = {
method: authData.method,
timestamp: Date.now(),
pubkey: authData.pubkey
};
switch (authData.method) {
case 'extension':
// For extensions, only store verification data - no secrets
authState.extensionVerification = {
constructor: authData.extension?.constructor?.name,
hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function',
hasSignEvent: typeof authData.extension?.signEvent === 'function'
};
console.log('🔐 AuthManager: Extension method - storing verification data only');
break;
case 'local':
// UNIFIED PLAINTEXT: Store secret key directly for maximum compatibility
if (authData.secret) {
authState.secret = authData.secret;
console.log('🔐 AuthManager: Local method - storing secret key in plaintext');
console.warn('🔐 SECURITY: Secret key stored unencrypted for developer convenience');
}
break;
case 'nip46':
// For NIP-46, store connection parameters (no secrets)
if (authData.signer) {
authState.nip46 = {
remotePubkey: authData.signer.remotePubkey,
relays: authData.signer.relays,
// Don't store secret - user will need to reconnect
};
console.log('🔐 AuthManager: NIP-46 method - storing connection parameters');
}
break;
case 'readonly':
// Read-only mode has no secrets to store
console.log('🔐 AuthManager: Read-only method - storing basic auth state');
break;
default:
throw new Error('Unknown auth method: ' + authData.method);
}
this.storage.setItem(this.storageKey, JSON.stringify(authState));
this.currentAuthState = authState;
console.log('🔐 AuthManager: Auth state saved successfully for method:', authData.method);
} catch (error) {
console.error('🔐 AuthManager: Failed to save auth state:', error);
throw error;
}
}
// Restore authentication state on page load
async restoreAuthState() {
try {
console.log('🔍 AuthManager: === restoreAuthState START ===');
console.log('🔍 AuthManager: storageKey:', this.storageKey);
const stored = this.storage.getItem(this.storageKey);
console.log('🔍 AuthManager: Storage raw value:', stored);
if (!stored) {
console.log('🔍 AuthManager: ❌ No stored auth state found');
return null;
}
const authState = JSON.parse(stored);
console.log('🔍 AuthManager: ✅ Parsed stored auth state:', authState);
console.log('🔍 AuthManager: Method:', authState.method);
console.log('🔍 AuthManager: Timestamp:', authState.timestamp);
console.log('🔍 AuthManager: Age (ms):', Date.now() - authState.timestamp);
// Check if stored state is too old (24 hours for most methods, 1 hour for extensions)
const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
console.log('🔍 AuthManager: Max age for method:', maxAge, 'ms');
if (Date.now() - authState.timestamp > maxAge) {
console.log('🔍 AuthManager: ❌ Stored auth state expired, clearing');
this.clearAuthState();
return null;
}
console.log('🔍 AuthManager: ✅ Auth state not expired, attempting restore for method:', authState.method);
let result;
switch (authState.method) {
case 'extension':
console.log('🔍 AuthManager: Calling _restoreExtensionAuth...');
result = await this._restoreExtensionAuth(authState);
break;
case 'local':
console.log('🔍 AuthManager: Calling _restoreLocalAuth...');
result = await this._restoreLocalAuth(authState);
break;
case 'nip46':
console.log('🔍 AuthManager: Calling _restoreNip46Auth...');
result = await this._restoreNip46Auth(authState);
break;
case 'readonly':
console.log('🔍 AuthManager: Calling _restoreReadonlyAuth...');
result = await this._restoreReadonlyAuth(authState);
break;
default:
console.warn('🔍 AuthManager: ❌ Unknown auth method in stored state:', authState.method);
return null;
}
console.log('🔍 AuthManager: Restore method result:', result);
console.log('🔍 AuthManager: === restoreAuthState END ===');
return result;
} catch (error) {
console.error('🔍 AuthManager: ❌ Failed to restore auth state:', error);
console.error('🔍 AuthManager: Error stack:', error.stack);
this.clearAuthState(); // Clear corrupted state
return null;
}
}
async _restoreExtensionAuth(authState) {
console.log('🔍 AuthManager: === _restoreExtensionAuth START ===');
console.log('🔍 AuthManager: authState:', authState);
console.log('🔍 AuthManager: window.nostr available:', !!window.nostr);
console.log('🔍 AuthManager: window.nostr constructor:', window.nostr?.constructor?.name);
// SMART EXTENSION WAITING SYSTEM
// Extensions often load after our library, so we need to wait for them
const extension = await this._waitForExtension(authState, 3000); // Wait up to 3 seconds
if (!extension) {
console.log('🔍 AuthManager: ❌ No extension found after waiting');
return null;
}
console.log('🔍 AuthManager: ✅ Extension found:', extension.constructor?.name);
try {
// Verify extension still works and has same pubkey
const currentPubkey = await extension.getPublicKey();
if (currentPubkey !== authState.pubkey) {
console.log('🔍 AuthManager: ❌ Extension pubkey changed, not restoring');
console.log('🔍 AuthManager: Expected:', authState.pubkey);
console.log('🔍 AuthManager: Got:', currentPubkey);
return null;
}
console.log('🔍 AuthManager: ✅ Extension auth restored successfully');
return {
method: 'extension',
pubkey: authState.pubkey,
extension: extension
};
} catch (error) {
console.log('🔍 AuthManager: ❌ Extension verification failed:', error);
return null;
}
}
// Smart extension waiting system - polls multiple locations for extensions
async _waitForExtension(authState, maxWaitMs = 3000) {
console.log('🔍 AuthManager: === _waitForExtension START ===');
console.log('🔍 AuthManager: maxWaitMs:', maxWaitMs);
console.log('🔍 AuthManager: Looking for extension with constructor:', authState.extensionVerification?.constructor);
const startTime = Date.now();
const pollInterval = 100; // Check every 100ms
// Extension locations to check (in priority order)
const extensionLocations = [
{ path: 'window.nostr', getter: () => window.nostr },
{ path: 'navigator.nostr', getter: () => navigator?.nostr },
{ path: 'window.navigator?.nostr', getter: () => window.navigator?.nostr },
{ path: 'window.alby?.nostr', getter: () => window.alby?.nostr },
{ path: 'window.webln?.nostr', getter: () => window.webln?.nostr },
{ path: 'window.nos2x', getter: () => window.nos2x },
{ path: 'window.flamingo?.nostr', getter: () => window.flamingo?.nostr },
{ path: 'window.mutiny?.nostr', getter: () => window.mutiny?.nostr }
];
while (Date.now() - startTime < maxWaitMs) {
console.log('🔍 AuthManager: Polling for extensions... (elapsed:', Date.now() - startTime, 'ms)');
// If our facade is currently installed and blocking, temporarily remove it
let facadeRemoved = false;
let originalNostr = null;
if (window.nostr?.constructor?.name === 'WindowNostr') {
console.log('🔍 AuthManager: Temporarily removing our facade to check for real extensions');
originalNostr = window.nostr;
window.nostr = window.nostr.existingNostr || undefined;
facadeRemoved = true;
}
try {
// Check all extension locations
for (const location of extensionLocations) {
try {
const extension = location.getter();
console.log('🔍 AuthManager: Checking', location.path, ':', !!extension, extension?.constructor?.name);
if (this._isValidExtensionForRestore(extension, authState)) {
console.log('🔍 AuthManager: ✅ Found matching extension at', location.path);
// Restore facade if we removed it
if (facadeRemoved && originalNostr) {
console.log('🔍 AuthManager: Restoring facade after finding extension');
window.nostr = originalNostr;
}
return extension;
}
} catch (error) {
console.log('🔍 AuthManager: Error checking', location.path, ':', error.message);
}
}
// Restore facade if we removed it and haven't found an extension yet
if (facadeRemoved && originalNostr) {
window.nostr = originalNostr;
facadeRemoved = false;
}
} catch (error) {
console.error('🔍 AuthManager: Error during extension polling:', error);
// Restore facade if we removed it
if (facadeRemoved && originalNostr) {
window.nostr = originalNostr;
}
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
console.log('🔍 AuthManager: ❌ Extension waiting timeout after', maxWaitMs, 'ms');
return null;
}
// Check if an extension is valid for restoration
_isValidExtensionForRestore(extension, authState) {
if (!extension || typeof extension !== 'object') {
return false;
}
// Must have required Nostr methods
if (typeof extension.getPublicKey !== 'function' ||
typeof extension.signEvent !== 'function') {
return false;
}
// Must not be our own classes
const constructorName = extension.constructor?.name;
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
return false;
}
// Must not be NostrTools
if (extension === window.NostrTools) {
return false;
}
// If we have stored verification data, check constructor match
const verification = authState.extensionVerification;
if (verification && verification.constructor) {
if (constructorName !== verification.constructor) {
console.log('🔍 AuthManager: Constructor mismatch -',
'expected:', verification.constructor,
'got:', constructorName);
return false;
}
}
console.log('🔍 AuthManager: ✅ Extension validation passed for:', constructorName);
return true;
}
async _restoreLocalAuth(authState) {
console.log('🔐 AuthManager: === _restoreLocalAuth (Unified Plaintext) ===');
// Check for legacy encrypted format first
if (authState.encrypted) {
console.log('🔐 AuthManager: Detected LEGACY encrypted format - migrating to plaintext');
console.warn('🔐 SECURITY: Converting from encrypted to plaintext storage for compatibility');
// Try to decrypt legacy format
const sessionPassword = sessionStorage.getItem('nostr_session_key');
if (!sessionPassword) {
console.log('🔐 AuthManager: Legacy session password not found - user must re-login');
return null;
}
try {
console.warn('🔐 AuthManager: Legacy encryption system no longer supported - user must re-login');
this.clearAuthState(); // Clear legacy format
return null;
} catch (error) {
console.error('🔐 AuthManager: Legacy decryption failed:', error);
this.clearAuthState(); // Clear corrupted legacy format
return null;
}
}
// NEW UNIFIED PLAINTEXT FORMAT
if (!authState.secret) {
console.log('🔐 AuthManager: No secret found in plaintext format');
return null;
}
console.log('🔐 AuthManager: ✅ Local auth restored from plaintext storage');
console.warn('🔐 SECURITY: Secret key was stored unencrypted');
return {
method: 'local',
pubkey: authState.pubkey,
secret: authState.secret
};
}
async _restoreNip46Auth(authState) {
if (!authState.nip46) {
console.log('🔐 AuthManager: No NIP-46 data found');
return null;
}
// For NIP-46, we can't automatically restore the connection
// because it requires the user to re-authenticate with the remote signer
// Instead, we return the connection parameters so the UI can prompt for reconnection
console.log('🔐 AuthManager: NIP-46 connection data found, requires user reconnection');
return {
method: 'nip46',
pubkey: authState.pubkey,
requiresReconnection: true,
connectionData: authState.nip46
};
}
async _restoreReadonlyAuth(authState) {
console.log('🔐 AuthManager: Read-only auth restored successfully');
return {
method: 'readonly',
pubkey: authState.pubkey
};
}
// Clear stored authentication state
clearAuthState() {
this.storage.removeItem(this.storageKey);
sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key
this.currentAuthState = null;
console.log('🔐 AuthManager: Auth state cleared from unified storage');
}
// Check if we have valid stored auth
hasStoredAuth() {
const stored = this.storage.getItem(this.storageKey);
return !!stored;
}
// Get current auth method without full restoration
getStoredAuthMethod() {
try {
const stored = this.storage.getItem(this.storageKey);
if (!stored) return null;
const authState = JSON.parse(stored);
return authState.method;
} catch {
return null;
}
}
}
// ======================================
// Global Authentication Functions (Single Source of Truth)
// ======================================
// Global authentication state (single source of truth)
let globalAuthState = null;
let globalAuthManager = null;
// Initialize global auth manager (lazy initialization)
function getGlobalAuthManager() {
if (!globalAuthManager) {
// Default to localStorage for persistence across browser sessions
globalAuthManager = new AuthManager({ isolateSession: false });
}
return globalAuthManager;
}
// **UNIFIED GLOBAL FUNCTION**: Set authentication state (works for all methods)
function setAuthState(authData, options = {}) {
try {
console.log('🌐 setAuthState: Setting global auth state for method:', authData.method);
console.warn('🔐 SECURITY: Using unified plaintext storage for maximum compatibility');
// Store in memory
globalAuthState = authData;
// Store in browser storage using AuthManager
const authManager = new AuthManager(options);
authManager.saveAuthState(authData);
console.log('🌐 setAuthState: Auth state saved successfully');
} catch (error) {
console.error('🌐 setAuthState: Failed to save auth state:', error);
throw error;
}
}
// **UNIFIED GLOBAL FUNCTION**: Get authentication state (single source of truth)
function getAuthState() {
try {
// Always query from storage as the authoritative source
const authManager = getGlobalAuthManager();
const storageKey = 'nostr_login_lite_auth';
// Check both session and local storage for compatibility
let stored = null;
if (sessionStorage.getItem(storageKey)) {
stored = sessionStorage.getItem(storageKey);
} else if (localStorage.getItem(storageKey)) {
stored = localStorage.getItem(storageKey);
}
if (!stored) {
console.log('🌐 getAuthState: No auth state found in storage');
globalAuthState = null;
return null;
}
const authState = JSON.parse(stored);
console.log('🌐 getAuthState: Retrieved auth state:', authState.method);
// Update in-memory cache
globalAuthState = authState;
return authState;
} catch (error) {
console.error('🌐 getAuthState: Failed to get auth state:', error);
globalAuthState = null;
return null;
}
}
// **UNIFIED GLOBAL FUNCTION**: Clear authentication state (works for all methods)
function clearAuthState() {
try {
console.log('🌐 clearAuthState: Clearing global auth state');
// Clear in-memory state
globalAuthState = null;
// Clear from both storage types for thorough cleanup
const storageKey = 'nostr_login_lite_auth';
localStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem('nostr_session_key'); // Clear legacy session key
console.log('🌐 clearAuthState: Auth state cleared from all storage locations');
} catch (error) {
console.error('🌐 clearAuthState: Failed to clear auth state:', error);
}
}
// NIP-07 compliant window.nostr provider
class WindowNostr {
constructor(nostrLite, existingNostr = null, options = {}) {
this.nostrLite = nostrLite;
this.authState = null;
this.existingNostr = existingNostr;
this.authenticatedExtension = null;
this.options = options;
this._setupEventListeners();
}
// Restore authentication state on page load
async restoreAuthState() {
console.log('🔍 WindowNostr: === restoreAuthState ===');
try {
// Use simplified AuthManager for consistent restore logic
const authManager = new AuthManager(this.options);
const restoredAuth = await authManager.restoreAuthState();
if (restoredAuth) {
console.log('🔍 WindowNostr: ✅ Auth state restored:', restoredAuth.method);
this.authState = restoredAuth;
// Update global state
globalAuthState = restoredAuth;
// Dispatch restoration event
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('nlAuthRestored', {
detail: restoredAuth
}));
}
return restoredAuth;
} else {
console.log('🔍 WindowNostr: ❌ No auth state to restore');
return null;
}
} catch (error) {
console.error('🔍 WindowNostr: Auth restoration failed:', error);
return null;
}
}
_setupEventListeners() {
// Listen for authentication events to store auth state
if (typeof window !== 'undefined') {
window.addEventListener('nlMethodSelected', async (event) => {
console.log('🔍 WindowNostr: nlMethodSelected event received:', event.detail);
this.authState = event.detail;
// If extension method, capture the specific extension the user chose
if (event.detail.method === 'extension') {
this.authenticatedExtension = event.detail.extension;
console.log('🔍 WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
}
// Use unified global setAuthState function for all methods
try {
setAuthState(event.detail, this.options);
console.log('🔍 WindowNostr: Auth state saved via unified setAuthState');
} catch (error) {
console.error('🔍 WindowNostr: Failed to save auth state:', error);
}
});
window.addEventListener('nlLogout', () => {
console.log('🔍 WindowNostr: nlLogout event received');
this.authState = null;
this.authenticatedExtension = null;
// Clear from unified storage
clearAuthState();
console.log('🔍 WindowNostr: Auth state cleared via unified clearAuthState');
});
}
}
async getPublicKey() {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
switch (this.authState.method) {
case 'extension':
// Use the captured authenticated extension, not current window.nostr
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.getPublicKey();
case 'local':
case 'nip46':
return this.authState.pubkey;
case 'readonly':
throw new Error('Read-only mode - cannot get public key');
default:
throw new Error('Unsupported auth method: ' + this.authState.method);
}
}
async signEvent(event) {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot sign events');
}
switch (this.authState.method) {
case 'extension':
// Use the captured authenticated extension, not current window.nostr
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.signEvent(event);
case 'local': {
// Use nostr-tools to sign with local secret key
const { nip19, finalizeEvent } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
// Convert hex to Uint8Array
secretKey = this._hexToUint8Array(this.authState.secret);
}
return finalizeEvent(event, secretKey);
}
case 'nip46': {
// Use BunkerSigner for NIP-46
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.signEvent(event);
}
default:
throw new Error('Unsupported auth method: ' + this.authState.method);
}
}
async getRelays() {
// Return configured relays from nostr-lite options
return this.nostrLite.options?.relays || ['wss://relay.damus.io'];
}
get nip04() {
return {
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip04.encrypt(pubkey, plaintext);
}
case 'local': {
const { nip04, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return await nip04.encrypt(secretKey, pubkey, plaintext);
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip04Encrypt(pubkey, plaintext);
}
default:
throw new Error('Unsupported auth method: ' + this.authState.method);
}
},
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip04.decrypt(pubkey, ciphertext);
}
case 'local': {
const { nip04, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return await nip04.decrypt(secretKey, pubkey, ciphertext);
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip04Decrypt(pubkey, ciphertext);
}
default:
throw new Error('Unsupported auth method: ' + this.authState.method);
}
}
};
}
get nip44() {
return {
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.encrypt(pubkey, plaintext);
}
case 'local': {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
}
default:
throw new Error('Unsupported auth method: ' + this.authState.method);
}
},
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.decrypt(pubkey, ciphertext);
}
case 'local': {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
}
return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
}
default:
throw new Error('Unsupported auth method: ' + this.authState.method);
}
}
};
}
_hexToUint8Array(hex) {
if (hex.length % 2 !== 0) {
throw new Error('Invalid hex string length');
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
}
// Initialize and export
if (typeof window !== 'undefined') {
const nostrLite = new NostrLite();
// Export main API
window.NOSTR_LOGIN_LITE = {
init: (options) => nostrLite.init(options),
launch: (startScreen) => nostrLite.launch(startScreen),
logout: () => nostrLite.logout(),
// Embedded modal method
embed: (container, options) => nostrLite.embed(container, options),
// CSS-only theme management API
switchTheme: (themeName) => nostrLite.switchTheme(themeName),
getCurrentTheme: () => nostrLite.getCurrentTheme(),
getAvailableThemes: () => nostrLite.getAvailableThemes(),
// Floating tab management API
showFloatingTab: () => nostrLite.showFloatingTab(),
hideFloatingTab: () => nostrLite.hideFloatingTab(),
toggleFloatingTab: () => nostrLite.toggleFloatingTab(),
updateFloatingTab: (options) => nostrLite.updateFloatingTab(options),
getFloatingTabState: () => nostrLite.getFloatingTabState(),
// Global authentication state management (single source of truth)
setAuthState: setAuthState,
getAuthState: getAuthState,
clearAuthState: clearAuthState,
// Expose for debugging
_extensionBridge: nostrLite.extensionBridge,
_instance: nostrLite
};
console.log('NOSTR_LOGIN_LITE: Library loaded and ready');
console.log('NOSTR_LOGIN_LITE: Use window.NOSTR_LOGIN_LITE.init(options) to initialize');
console.log('NOSTR_LOGIN_LITE: Detected', nostrLite.extensionBridge.getExtensionCount(), 'browser extensions');
console.warn('🔐 SECURITY: Unified plaintext storage enabled for maximum developer usability');
} else {
// Node.js environment
module.exports = { NostrLite };
}
`;
// Write the complete bundle
fs.writeFileSync(outputPath, bundle, 'utf8');
const sizeKB = (bundle.length / 1024).toFixed(2);
console.log('\n✅ nostr-lite.js bundle created: ' + outputPath);
console.log('📏 Bundle size: ' + sizeKB + ' KB');
console.log('📄 Total lines: ' + bundle.split('\n').length);
// Check what's included
const hasModal = bundle.includes('class Modal');
const hasNostrLite = bundle.includes('NOSTR_LOGIN_LITE');
const hasThemeCss = bundle.includes('THEME_CSS');
console.log('\n📋 Bundle contents:');
console.log(' Modal UI: ' + (hasModal ? '✅ Included' : '❌ Missing'));
console.log(' NOSTR_LOGIN_LITE: ' + (hasNostrLite ? '✅ Included' : '❌ Missing'));
console.log(' CSS-Only Themes: ' + (hasThemeCss ? '✅ Included' : '❌ Missing'));
console.log(' Extension Bridge: ✅ Included');
console.log(' Window.nostr facade: ✅ Included');
console.log('\n📋 Two-file architecture:');
console.log(' 1. nostr.bundle.js (official nostr-tools - 220KB)');
console.log(' 2. nostr-lite.js (NOSTR_LOGIN_LITE with CSS-only themes - ' + sizeKB + 'KB)');
return bundle;
}
// Run if called directly
if (typeof require !== 'undefined' && require.main === module) {
createNostrLoginLiteBundle();
}
module.exports = { createNostrLoginLiteBundle };