Single Source of Truth Architecture - Complete authentication state management with storage-based getAuthState() as sole authoritative source
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user