Single Source of Truth Architecture - Complete authentication state management with storage-based getAuthState() as sole authoritative source

This commit is contained in:
Your Name
2025-09-20 10:39:43 -04:00
parent 8f34c2de73
commit ccff136edb
11 changed files with 1530 additions and 200 deletions

View File

@@ -191,8 +191,6 @@ class FloatingTab {
...options
};
this.isAuthenticated = false;
this.userInfo = null;
this.userProfile = null;
this.container = null;
this.isVisible = false;
@@ -211,6 +209,12 @@ class FloatingTab {
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');
@@ -286,24 +290,79 @@ class FloatingTab {
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
});
}
async _handleClick() {
// 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');
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
const authState = this._getAuthState();
if (authState && this.options.behavior.showUserInfo) {
// Show user menu or profile options
this._showUserMenu();
} else {
// Check if extension is available for direct login
if (window.nostr && this._isRealExtension(window.nostr)) {
console.log('FloatingTab: Extension available, attempting direct extension login');
await this._tryExtensionLogin(window.nostr);
} else {
// Open login modal
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
// Always open login modal (consistent with login buttons)
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
}
@@ -385,46 +444,56 @@ class FloatingTab {
async _handleAuth(authData) {
console.log('🔍 FloatingTab: === _handleAuth START ===');
console.log('🔍 FloatingTab: authData received:', authData);
console.log('🔍 FloatingTab: Current isAuthenticated before:', this.isAuthenticated);
this.isAuthenticated = true;
this.userInfo = authData;
console.log('🔍 FloatingTab: Set isAuthenticated to true');
console.log('🔍 FloatingTab: Set userInfo to:', this.userInfo);
// 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;
// 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');
}
} else {
console.log('🔍 FloatingTab: getUserInfo disabled or no pubkey, skipping profile fetch');
}
console.log('🔍 FloatingTab: hideWhenAuthenticated option:', this.options.behavior.hideWhenAuthenticated);
if (this.options.behavior.hideWhenAuthenticated) {
console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true)');
this.hide();
} else {
console.log('🔍 FloatingTab: Updating appearance (hideWhenAuthenticated=false)');
this._updateAppearance();
}
// 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.isAuthenticated = false;
this.userInfo = null;
this.userProfile = null;
if (this.options.behavior.hideWhenAuthenticated) {
@@ -491,8 +560,12 @@ class FloatingTab {
_updateAppearance() {
if (!this.container) return;
// Query authoritative source for all state information
const authState = this._getAuthState();
const isAuthenticated = authState !== null;
// Update content
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
if (isAuthenticated && this.options.behavior.showUserInfo) {
let display;
// Use profile name if available, otherwise fall back to pubkey
@@ -501,11 +574,11 @@ class FloatingTab {
display = this.options.appearance.iconOnly
? userName.slice(0, 8)
: userName;
} else if (this.userInfo?.pubkey) {
} else if (authState?.pubkey) {
// Fallback to pubkey display
display = this.options.appearance.iconOnly
? this.userInfo.pubkey.slice(0, 6)
: \`\${this.userInfo.pubkey.slice(0, 6)}...\`;
? authState.pubkey.slice(0, 6)
: \`\${authState.pubkey.slice(0, 6)}...\`;
} else {
display = this.options.appearance.iconOnly ? 'User' : 'Authenticated';
}
@@ -688,10 +761,11 @@ class FloatingTab {
// Get current state
getState() {
const authState = this._getAuthState();
return {
isVisible: this.isVisible,
isAuthenticated: this.isAuthenticated,
userInfo: this.userInfo,
isAuthenticated: !!authState,
userInfo: authState,
options: this.options
};
}
@@ -766,6 +840,7 @@ class NostrLite {
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,
@@ -870,7 +945,7 @@ class NostrLite {
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr);
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name);
const facade = new WindowNostr(this, existingNostr);
const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession });
window.nostr = facade;
this.facadeInstalled = true;
@@ -1008,7 +1083,7 @@ class NostrLite {
console.log('🔍 NOSTR_LOGIN_LITE: === _attemptExtensionRestore START ===');
// Use a simple AuthManager instance for extension persistence
const authManager = new AuthManager();
const authManager = new AuthManager({ isolateSession: this.options?.isolateSession });
const storedAuth = await authManager.restoreAuthState();
if (!storedAuth || storedAuth.method !== 'extension') {
@@ -1291,9 +1366,18 @@ class CryptoUtils {
// Unified authentication state manager
class AuthManager {
constructor() {
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');
}
}
// Save authentication state with method-specific security
@@ -1353,7 +1437,7 @@ class AuthManager {
throw new Error(\`Unknown auth method: \${authData.method}\`);
}
localStorage.setItem(this.storageKey, JSON.stringify(authState));
this.storage.setItem(this.storageKey, JSON.stringify(authState));
this.currentAuthState = authState;
console.log('AuthManager: Auth state saved for method:', authData.method);
@@ -1369,7 +1453,7 @@ class AuthManager {
console.log('🔍 AuthManager: === restoreAuthState START ===');
console.log('🔍 AuthManager: storageKey:', this.storageKey);
const stored = localStorage.getItem(this.storageKey);
const stored = this.storage.getItem(this.storageKey);
console.log('🔍 AuthManager: localStorage raw value:', stored);
if (!stored) {
@@ -1656,7 +1740,7 @@ class AuthManager {
// Clear stored authentication state
clearAuthState() {
localStorage.removeItem(this.storageKey);
this.storage.removeItem(this.storageKey);
sessionStorage.removeItem('nostr_session_key');
this.currentAuthState = null;
console.log('AuthManager: Auth state cleared');
@@ -1671,14 +1755,14 @@ class AuthManager {
// Check if we have valid stored auth
hasStoredAuth() {
const stored = localStorage.getItem(this.storageKey);
const stored = this.storage.getItem(this.storageKey);
return !!stored;
}
// Get current auth method without full restoration
getStoredAuthMethod() {
try {
const stored = localStorage.getItem(this.storageKey);
const stored = this.storage.getItem(this.storageKey);
if (!stored) return null;
const authState = JSON.parse(stored);
@@ -1696,7 +1780,7 @@ class WindowNostr {
this.authState = null;
this.existingNostr = existingNostr;
this.authenticatedExtension = null;
this.authManager = new AuthManager();
this.authManager = new AuthManager({ isolateSession: nostrLite.options?.isolateSession });
this._setupEventListeners();
}
@@ -1974,17 +2058,18 @@ class WindowNostr {
get nip44() {
return {
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
const authState = getAuthState();
if (!authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
if (authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
switch (authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
const ext = this.authenticatedExtension || authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.encrypt(pubkey, plaintext);
}
@@ -1993,40 +2078,41 @@ class WindowNostr {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
if (authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
secretKey = this._hexToUint8Array(authState.secret);
}
return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
if (!authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
return await authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
}
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
throw new Error('Unsupported auth method: ' + authState.method);
}
},
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
const authState = getAuthState();
if (!authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
if (authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
switch (authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
const ext = this.authenticatedExtension || authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.decrypt(pubkey, ciphertext);
}
@@ -2035,25 +2121,25 @@ class WindowNostr {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
if (authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
secretKey = this._hexToUint8Array(authState.secret);
}
return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
if (!authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
return await authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
}
default:
throw new Error(\`Unsupported auth method: \${this.authState.method}\`);
throw new Error('Unsupported auth method: ' + authState.method);
}
}
};
@@ -2071,6 +2157,60 @@ class WindowNostr {
}
}
// ======================================
// Global Authentication State Manager - Single Source of Truth
// ======================================
// Storage-based authentication state - works regardless of extension presence
function getAuthState() {
try {
console.log('🔍 getAuthState: === GLOBAL AUTH STATE CHECK ===');
const storageKey = 'nostr_login_lite_auth';
let stored = null;
let storageType = null;
// Check sessionStorage first (per-window isolation), then localStorage
if (sessionStorage.getItem(storageKey)) {
stored = sessionStorage.getItem(storageKey);
storageType = 'sessionStorage';
console.log('🔍 getAuthState: Found auth in sessionStorage');
} else if (localStorage.getItem(storageKey)) {
stored = localStorage.getItem(storageKey);
storageType = 'localStorage';
console.log('🔍 getAuthState: Found auth in localStorage');
}
if (!stored) {
console.log('🔍 getAuthState: ❌ No stored auth state found');
return null;
}
const authState = JSON.parse(stored);
console.log('🔍 getAuthState: ✅ Parsed stored auth state from', storageType);
console.log('🔍 getAuthState: Method:', authState.method);
console.log('🔍 getAuthState: Pubkey:', authState.pubkey);
console.log('🔍 getAuthState: Age (ms):', Date.now() - authState.timestamp);
// Check if auth state is expired
const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
if (Date.now() - authState.timestamp > maxAge) {
console.log('🔍 getAuthState: ❌ Auth state expired, clearing');
sessionStorage.removeItem(storageKey);
localStorage.removeItem(storageKey);
return null;
}
console.log('🔍 getAuthState: ✅ Valid auth state found');
return authState;
} catch (error) {
console.error('🔍 getAuthState: ❌ Error reading auth state:', error);
return null;
}
}
// Initialize and export
if (typeof window !== 'undefined') {
const nostrLite = new NostrLite();
@@ -2096,6 +2236,9 @@ if (typeof window !== 'undefined') {
updateFloatingTab: (options) => nostrLite.updateFloatingTab(options),
getFloatingTabState: () => nostrLite.getFloatingTabState(),
// GLOBAL AUTHENTICATION STATE API - Single Source of Truth
getAuthState: getAuthState,
// Expose for debugging
_extensionBridge: nostrLite.extensionBridge,
_instance: nostrLite

View File

@@ -8,7 +8,7 @@
* Two-file architecture:
* 1. Load nostr.bundle.js (official nostr-tools bundle)
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
* Generated on: 2025-09-19T19:39:40.411Z
* Generated on: 2025-09-20T14:23:53.897Z
*/
// Verify dependencies are loaded
@@ -2158,8 +2158,6 @@ class FloatingTab {
...options
};
this.isAuthenticated = false;
this.userInfo = null;
this.userProfile = null;
this.container = null;
this.isVisible = false;
@@ -2178,6 +2176,12 @@ class FloatingTab {
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');
@@ -2253,24 +2257,79 @@ class FloatingTab {
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
});
}
async _handleClick() {
// 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');
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
const authState = this._getAuthState();
if (authState && this.options.behavior.showUserInfo) {
// Show user menu or profile options
this._showUserMenu();
} else {
// Check if extension is available for direct login
if (window.nostr && this._isRealExtension(window.nostr)) {
console.log('FloatingTab: Extension available, attempting direct extension login');
await this._tryExtensionLogin(window.nostr);
} else {
// Open login modal
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
// Always open login modal (consistent with login buttons)
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
}
@@ -2352,46 +2411,56 @@ class FloatingTab {
async _handleAuth(authData) {
console.log('🔍 FloatingTab: === _handleAuth START ===');
console.log('🔍 FloatingTab: authData received:', authData);
console.log('🔍 FloatingTab: Current isAuthenticated before:', this.isAuthenticated);
this.isAuthenticated = true;
this.userInfo = authData;
console.log('🔍 FloatingTab: Set isAuthenticated to true');
console.log('🔍 FloatingTab: Set userInfo to:', this.userInfo);
// 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;
// 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');
}
} else {
console.log('🔍 FloatingTab: getUserInfo disabled or no pubkey, skipping profile fetch');
}
console.log('🔍 FloatingTab: hideWhenAuthenticated option:', this.options.behavior.hideWhenAuthenticated);
if (this.options.behavior.hideWhenAuthenticated) {
console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true)');
this.hide();
} else {
console.log('🔍 FloatingTab: Updating appearance (hideWhenAuthenticated=false)');
this._updateAppearance();
}
// 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.isAuthenticated = false;
this.userInfo = null;
this.userProfile = null;
if (this.options.behavior.hideWhenAuthenticated) {
@@ -2458,8 +2527,12 @@ class FloatingTab {
_updateAppearance() {
if (!this.container) return;
// Query authoritative source for all state information
const authState = this._getAuthState();
const isAuthenticated = authState !== null;
// Update content
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
if (isAuthenticated && this.options.behavior.showUserInfo) {
let display;
// Use profile name if available, otherwise fall back to pubkey
@@ -2468,11 +2541,11 @@ class FloatingTab {
display = this.options.appearance.iconOnly
? userName.slice(0, 8)
: userName;
} else if (this.userInfo?.pubkey) {
} else if (authState?.pubkey) {
// Fallback to pubkey display
display = this.options.appearance.iconOnly
? this.userInfo.pubkey.slice(0, 6)
: `${this.userInfo.pubkey.slice(0, 6)}...`;
? authState.pubkey.slice(0, 6)
: `${authState.pubkey.slice(0, 6)}...`;
} else {
display = this.options.appearance.iconOnly ? 'User' : 'Authenticated';
}
@@ -2655,10 +2728,11 @@ class FloatingTab {
// Get current state
getState() {
const authState = this._getAuthState();
return {
isVisible: this.isVisible,
isAuthenticated: this.isAuthenticated,
userInfo: this.userInfo,
isAuthenticated: !!authState,
userInfo: authState,
options: this.options
};
}
@@ -2733,6 +2807,7 @@ class NostrLite {
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,
@@ -2837,7 +2912,7 @@ class NostrLite {
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr);
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name);
const facade = new WindowNostr(this, existingNostr);
const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession });
window.nostr = facade;
this.facadeInstalled = true;
@@ -2975,7 +3050,7 @@ class NostrLite {
console.log('🔍 NOSTR_LOGIN_LITE: === _attemptExtensionRestore START ===');
// Use a simple AuthManager instance for extension persistence
const authManager = new AuthManager();
const authManager = new AuthManager({ isolateSession: this.options?.isolateSession });
const storedAuth = await authManager.restoreAuthState();
if (!storedAuth || storedAuth.method !== 'extension') {
@@ -3258,9 +3333,18 @@ class CryptoUtils {
// Unified authentication state manager
class AuthManager {
constructor() {
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');
}
}
// Save authentication state with method-specific security
@@ -3320,7 +3404,7 @@ class AuthManager {
throw new Error(`Unknown auth method: ${authData.method}`);
}
localStorage.setItem(this.storageKey, JSON.stringify(authState));
this.storage.setItem(this.storageKey, JSON.stringify(authState));
this.currentAuthState = authState;
console.log('AuthManager: Auth state saved for method:', authData.method);
@@ -3336,7 +3420,7 @@ class AuthManager {
console.log('🔍 AuthManager: === restoreAuthState START ===');
console.log('🔍 AuthManager: storageKey:', this.storageKey);
const stored = localStorage.getItem(this.storageKey);
const stored = this.storage.getItem(this.storageKey);
console.log('🔍 AuthManager: localStorage raw value:', stored);
if (!stored) {
@@ -3623,7 +3707,7 @@ class AuthManager {
// Clear stored authentication state
clearAuthState() {
localStorage.removeItem(this.storageKey);
this.storage.removeItem(this.storageKey);
sessionStorage.removeItem('nostr_session_key');
this.currentAuthState = null;
console.log('AuthManager: Auth state cleared');
@@ -3638,14 +3722,14 @@ class AuthManager {
// Check if we have valid stored auth
hasStoredAuth() {
const stored = localStorage.getItem(this.storageKey);
const stored = this.storage.getItem(this.storageKey);
return !!stored;
}
// Get current auth method without full restoration
getStoredAuthMethod() {
try {
const stored = localStorage.getItem(this.storageKey);
const stored = this.storage.getItem(this.storageKey);
if (!stored) return null;
const authState = JSON.parse(stored);
@@ -3663,7 +3747,7 @@ class WindowNostr {
this.authState = null;
this.existingNostr = existingNostr;
this.authenticatedExtension = null;
this.authManager = new AuthManager();
this.authManager = new AuthManager({ isolateSession: nostrLite.options?.isolateSession });
this._setupEventListeners();
}
@@ -3941,17 +4025,18 @@ class WindowNostr {
get nip44() {
return {
encrypt: async (pubkey, plaintext) => {
if (!this.authState) {
const authState = getAuthState();
if (!authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
if (authState.method === 'readonly') {
throw new Error('Read-only mode - cannot encrypt');
}
switch (this.authState.method) {
switch (authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
const ext = this.authenticatedExtension || authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.encrypt(pubkey, plaintext);
}
@@ -3960,40 +4045,41 @@ class WindowNostr {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
if (authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
secretKey = this._hexToUint8Array(authState.secret);
}
return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
if (!authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
return await authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
}
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
throw new Error('Unsupported auth method: ' + authState.method);
}
},
decrypt: async (pubkey, ciphertext) => {
if (!this.authState) {
const authState = getAuthState();
if (!authState) {
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
}
if (this.authState.method === 'readonly') {
if (authState.method === 'readonly') {
throw new Error('Read-only mode - cannot decrypt');
}
switch (this.authState.method) {
switch (authState.method) {
case 'extension': {
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
const ext = this.authenticatedExtension || authState.extension || this.existingNostr;
if (!ext) throw new Error('Extension not available');
return await ext.nip44.decrypt(pubkey, ciphertext);
}
@@ -4002,25 +4088,25 @@ class WindowNostr {
const { nip44, nip19 } = window.NostrTools;
let secretKey;
if (this.authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(this.authState.secret);
if (authState.secret.startsWith('nsec')) {
const decoded = nip19.decode(authState.secret);
secretKey = decoded.data;
} else {
secretKey = this._hexToUint8Array(this.authState.secret);
secretKey = this._hexToUint8Array(authState.secret);
}
return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey));
}
case 'nip46': {
if (!this.authState.signer?.bunkerSigner) {
if (!authState.signer?.bunkerSigner) {
throw new Error('NIP-46 signer not available');
}
return await this.authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
return await authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
}
default:
throw new Error(`Unsupported auth method: ${this.authState.method}`);
throw new Error('Unsupported auth method: ' + authState.method);
}
}
};
@@ -4038,6 +4124,60 @@ class WindowNostr {
}
}
// ======================================
// Global Authentication State Manager - Single Source of Truth
// ======================================
// Storage-based authentication state - works regardless of extension presence
function getAuthState() {
try {
console.log('🔍 getAuthState: === GLOBAL AUTH STATE CHECK ===');
const storageKey = 'nostr_login_lite_auth';
let stored = null;
let storageType = null;
// Check sessionStorage first (per-window isolation), then localStorage
if (sessionStorage.getItem(storageKey)) {
stored = sessionStorage.getItem(storageKey);
storageType = 'sessionStorage';
console.log('🔍 getAuthState: Found auth in sessionStorage');
} else if (localStorage.getItem(storageKey)) {
stored = localStorage.getItem(storageKey);
storageType = 'localStorage';
console.log('🔍 getAuthState: Found auth in localStorage');
}
if (!stored) {
console.log('🔍 getAuthState: ❌ No stored auth state found');
return null;
}
const authState = JSON.parse(stored);
console.log('🔍 getAuthState: ✅ Parsed stored auth state from', storageType);
console.log('🔍 getAuthState: Method:', authState.method);
console.log('🔍 getAuthState: Pubkey:', authState.pubkey);
console.log('🔍 getAuthState: Age (ms):', Date.now() - authState.timestamp);
// Check if auth state is expired
const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
if (Date.now() - authState.timestamp > maxAge) {
console.log('🔍 getAuthState: ❌ Auth state expired, clearing');
sessionStorage.removeItem(storageKey);
localStorage.removeItem(storageKey);
return null;
}
console.log('🔍 getAuthState: ✅ Valid auth state found');
return authState;
} catch (error) {
console.error('🔍 getAuthState: ❌ Error reading auth state:', error);
return null;
}
}
// Initialize and export
if (typeof window !== 'undefined') {
const nostrLite = new NostrLite();
@@ -4063,6 +4203,9 @@ if (typeof window !== 'undefined') {
updateFloatingTab: (options) => nostrLite.updateFloatingTab(options),
getFloatingTabState: () => nostrLite.getFloatingTabState(),
// GLOBAL AUTHENTICATION STATE API - Single Source of Truth
getAuthState: getAuthState,
// Expose for debugging
_extensionBridge: nostrLite.extensionBridge,
_instance: nostrLite