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

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
# Dependencies
node_modules/
nostr-tools/
# IDE and OS files
.idea/

View File

@@ -1,51 +1,47 @@
Nostr_Login_Lite
===========
## Floating Tab API
## API
Configure persistent floating tab for login/logout:
Configure for login/logout:
```javascript
await NOSTR_LOGIN_LITE.init({
// Set the initial theme (default: 'default')
theme: 'dark', // Choose from 'default' or 'dark'
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
// Standard configuration options
methods: {
extension: true,
local: true,
seedphrase: true, // ✅ Must be explicitly enabled
readonly: true,
connect: true,
otp: true
otp: false
},
// Floating tab configuration (now uses theme-aware text icons)
floatingTab: {
enabled: true,
hPosition: 0.95, // 0.0-1.0 or '95%' from left
vPosition: 0.5, // 0.0-1.0 or '50%' from top
getUserInfo: true, // Fetch user profile name from relays
getUserRelay: [ // Relays for profile queries
'wss://relay.damus.io',
'wss://nos.lol'
],
hPosition: 0.95, // Near right edge
vPosition: 0.1, // Near top
appearance: {
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
theme: 'auto', // 'auto' follows main theme
icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
text: 'Login'
style: 'pill',
icon: '[LOGIN]',
text: 'Sign In',
iconOnly: false
},
behavior: {
hideWhenAuthenticated: false,
hideWhenAuthenticated: false, // Keep visible after login
showUserInfo: true,
autoSlide: true
},
animation: {
slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down'
}
getUserInfo: true, // ✅ Fetch user profiles
getUserRelay: ['wss://relay.laantungir.net'] // Custom relays for profiles
}
});
// After initialization, you can switch themes dynamically:
NOSTR_LOGIN_LITE.switchTheme('dark');
NOSTR_LOGIN_LITE.switchTheme('default');
@@ -86,3 +82,31 @@ const modal = NOSTR_LOGIN_LITE.embed('#login-container', {
```
Container can be CSS selector or DOM element. Modal renders inline without backdrop overlay.
## Logout API
To log out users and clear authentication state:
```javascript
// Unified logout method - works for all authentication methods
window.NOSTR_LOGIN_LITE.logout();
```
This will:
- Clear persistent authentication data from localStorage
- Dispatch `nlLogout` event for custom cleanup
- Reset the authentication state across all components
### Event Handling
Listen for logout events in your application:
```javascript
window.addEventListener('nlLogout', () => {
console.log('User logged out');
// Clear your application's UI state
// Redirect to login page, etc.
});
```
The logout system works consistently across all authentication methods (extension, local keys, NIP-46, etc.) and all UI components (floating tab, modal, embedded).

View File

@@ -35,7 +35,7 @@
}
#login-button:hover {
background: #0052a3;
opacity: 0.8;
}
</style>
</head>
@@ -49,6 +49,9 @@
<script src="../lite/nostr-lite.js"></script>
<script>
let isAuthenticated = false;
let currentUser = null;
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
@@ -65,10 +68,66 @@
}
});
document.getElementById('login-button').addEventListener('click', () => {
window.NOSTR_LOGIN_LITE.launch('login');
});
// Listen for authentication events
window.addEventListener('nlMethodSelected', handleAuthEvent);
window.addEventListener('nlLogout', handleLogoutEvent);
// Check for existing authentication state
checkAuthState();
// Initialize button
updateButtonState();
});
function handleAuthEvent(event) {
const { pubkey, method } = event.detail;
console.log(`Authenticated with ${method}, pubkey: ${pubkey}`);
isAuthenticated = true;
currentUser = event.detail;
updateButtonState();
}
function handleLogoutEvent() {
console.log('Logout event received');
isAuthenticated = false;
currentUser = null;
updateButtonState();
}
function checkAuthState() {
// Check if user is already authenticated (from persistent storage)
try {
// Try to get public key - this will work if already authenticated
window.nostr.getPublicKey().then(pubkey => {
console.log('Found existing authentication, pubkey:', pubkey);
isAuthenticated = true;
currentUser = { pubkey, method: 'persistent' };
updateButtonState();
}).catch(error => {
console.log('No existing authentication found:', error.message);
// User is not authenticated, button stays in login state
});
} catch (error) {
console.log('No existing authentication found');
// User is not authenticated, button stays in login state
}
}
function updateButtonState() {
const button = document.getElementById('login-button');
if (isAuthenticated) {
button.textContent = 'Logout';
button.onclick = () => window.NOSTR_LOGIN_LITE.logout();
button.style.background = '#dc3545'; // Red for logout
} else {
button.textContent = 'Login';
button.onclick = () => window.NOSTR_LOGIN_LITE.launch('login');
button.style.background = '#0066cc'; // Blue for login
}
}
</script>
</body>

View File

@@ -41,6 +41,7 @@
methods: {
extension: true,
local: true,
seedphrase: true,
readonly: true,
connect: true,
remote: true,

View File

@@ -51,19 +51,20 @@
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
darkMode: false,
relays: [relayUrl, 'wss://relay.damus.io'],
methods: {
extension: true,
local: true,
readonly: true,
seedphrase: true,
connect: true, // Enables "Nostr Connect" (NIP-46)
remote: true, // Also needed for "Nostr Connect" compatibility
otp: true // Enables "DM/OTP"
},
floatingTab: {
enabled: true,
hPosition: 0.80, // 95% from left
vPosition: 0.01, // 50% from top (center)
hPosition: .98, // 95% from left
vPosition: 0, // 50% from top (center)
getUserInfo: true, // Fetch user profiles
getUserRelay: ['wss://relay.laantungir.net'], // Custom relays for profiles
appearance: {
style: 'minimal',
theme: 'auto',
@@ -88,6 +89,7 @@
console.log('SUCCESS', 'NOSTR_LOGIN_LITE initialized successfully');
window.addEventListener('nlMethodSelected', handleAuthEvent);
window.addEventListener('nlLogout', handleLogoutEvent);
} catch (error) {
console.log('ERROR', `Initialization failed: ${error.message}`);
@@ -97,7 +99,7 @@
function handleAuthEvent(event) {
const {pubkey, method, error } = event.detail;
const { pubkey, method, error } = event.detail;
console.log('INFO', `Auth event received: method=${method}`);
if (method && pubkey) {
@@ -112,6 +114,16 @@
}
}
function handleLogoutEvent() {
console.log('INFO', 'Logout event received');
// Clear local UI state
userPubkey = null;
document.getElementById('profile-name').textContent = '';
document.getElementById('profile-about').textContent = '';
document.getElementById('profile-pubkey').textContent = '';
document.getElementById('profile-picture').src = '';
}
// Load user profile using nostr-tools pool
async function loadUserProfile() {
if (!userPubkey) return;
@@ -171,7 +183,7 @@
async function logout() {
console.log('INFO', 'Logging out...');
try {
await nlLite.logout();
window.NOSTR_LOGIN_LITE.logout();
console.log('SUCCESS', 'Logged out successfully');
} catch (error) {
console.log('ERROR', `Logout failed: ${error.message}`);

View File

@@ -41,6 +41,7 @@
methods: {
extension: true,
local: true,
seedphrase:true,
readonly: true,
connect: true,
remote: true,

View File

@@ -0,0 +1,534 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Isolation Test - NOSTR LOGIN LITE</title>
<style>
body {
font-family: 'Courier New', monospace;
margin: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status-panel {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.info-box {
background: #e7f3ff;
border: 2px solid #0066cc;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.isolated-notice {
background: #f8d7da;
border: 2px solid #dc3545;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
button {
background: white;
color: black;
border: 2px solid black;
border-radius: 8px;
padding: 10px 20px;
margin: 5px;
cursor: pointer;
font-family: 'Courier New', monospace;
font-weight: bold;
}
button:hover {
border-color: red;
}
button:active {
background: red;
color: white;
}
.mode-indicator {
font-weight: bold;
padding: 8px 16px;
border-radius: 4px;
border: 2px solid #dc3545;
background: #f8d7da;
display: inline-block;
margin: 10px 0;
}
pre {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 10px;
overflow-x: auto;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Session Isolation Test</h1>
<div class="info-box">
<h3>📋 Test Instructions</h3>
<ol>
<li><strong>Isolated Session:</strong> Each tab/window has independent authentication</li>
<li>Login in this tab/window - it will persist on refresh</li>
<li>Open new windows/tabs - they will start unauthenticated</li>
<li>Login with different users in different windows simultaneously</li>
<li>Refresh any window - authentication persists within that window only</li>
</ol>
</div>
<div class="mode-indicator">
🔒 ISOLATED MODE (sessionStorage)
</div>
<div class="isolated-notice">
<strong>🚨 Session Isolation Active:</strong>
<p>This tab uses sessionStorage - authentication is isolated to this window only. Refreshing will maintain your login state, but other tabs/windows are independent.</p>
</div>
<div class="status-panel">
<h3>Authentication Status</h3>
<div id="auth-status">Not authenticated</div>
<div id="auth-details"></div>
</div>
<div>
<h3>Actions</h3>
<button onclick="login()">Login</button>
<button onclick="logout()">Logout</button>
<button onclick="checkStatus()">Check Status</button>
<button onclick="testSigning()">Test Signing</button>
<button onclick="openNewWindow()">Open New Window</button>
<button onclick="debugAuthentication()" style="border-color: orange;">Debug Auth State</button>
</div>
<div>
<h3>Storage Inspector</h3>
<button onclick="inspectStorage()">Inspect SessionStorage</button>
<button onclick="clearStorage()">Clear Session Storage</button>
<div id="storage-content"></div>
</div>
<div>
<h3>Test Results</h3>
<div id="results"></div>
</div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
let nostrLiteInstance = null;
// Initialize in isolated mode (always)
initializeIsolatedMode();
async function initializeIsolatedMode() {
try {
console.log('Initializing NOSTR_LOGIN_LITE in ISOLATED mode...');
nostrLiteInstance = await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
persistence: true,
isolateSession: true, // Always isolated - each tab/window independent
methods: {
extension: true,
local: true,
readonly: true,
connect: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 0.95,
vPosition: 0.1,
appearance: {
style: 'pill',
icon: '🔒',
text: 'ISOLATED',
iconOnly: false
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
}
}
});
checkStatus();
console.log('NOSTR_LOGIN_LITE initialized successfully in ISOLATED mode');
console.log('Authentication will persist on refresh within this tab only');
} catch (error) {
console.error('Failed to initialize NOSTR_LOGIN_LITE:', error);
document.getElementById('results').innerHTML =
`<div style="color: red;">Initialization Error: ${error.message}</div>`;
}
}
function login() {
window.NOSTR_LOGIN_LITE.launch('login');
}
function logout() {
window.NOSTR_LOGIN_LITE.logout();
setTimeout(checkStatus, 100);
}
function debugAuthentication() {
console.log('=== AUTHENTICATION DEBUG ===');
// Check global storage-based authentication state (SINGLE SOURCE OF TRUTH)
const authState = window.NOSTR_LOGIN_LITE.getAuthState();
console.log('🔍 GLOBAL getAuthState():', authState);
console.log('🔍 Derived isAuthenticated():', !!authState);
console.log('🔍 Derived getUserInfo():', authState);
// Check window.nostr (should sync with global state)
console.log('window.nostr exists:', !!window.nostr);
console.log('window.nostr constructor:', window.nostr?.constructor?.name);
console.log('window.nostr.authState (getter):', window.nostr?.authState);
// Check NOSTR_LOGIN_LITE instance
const instance = window.NOSTR_LOGIN_LITE?._instance;
console.log('NOSTR_LOGIN_LITE instance exists:', !!instance);
console.log('Instance hasExtension:', instance?.hasExtension);
console.log('Instance facadeInstalled:', instance?.facadeInstalled);
// Check floating tab state (now queries global getAuthState() only)
const floatingTab = instance?.floatingTab;
console.log('FloatingTab exists:', !!floatingTab);
if (floatingTab) {
const tabAuthState = floatingTab._getAuthState();
console.log('FloatingTab _getAuthState():', tabAuthState);
console.log('FloatingTab derived authenticated:', !!tabAuthState);
}
// Check session storage directly
const sessionKeys = [];
const storageKey = 'nostr_login_lite_auth';
const sessionAuthData = sessionStorage.getItem(storageKey);
const localAuthData = localStorage.getItem(storageKey);
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('nl_')) {
const value = sessionStorage.getItem(key);
sessionKeys.push({ key, valueLength: value?.length || 0, hasValue: !!value });
}
}
console.log('SessionStorage nl_ keys:', sessionKeys);
console.log('SessionStorage auth data:', !!sessionAuthData);
console.log('LocalStorage auth data:', !!localAuthData);
// Display debug results
let debugHTML = '<h4>🔍 Storage-Based Authentication Debug</h4>';
debugHTML += '<div style="font-family: monospace; font-size: 12px; background: #f8f9fa; padding: 10px; border-radius: 4px;">';
debugHTML += `<strong>🎯 GLOBAL getAuthState():</strong> ${!!authState} ${authState ? `(${authState.method})` : ''}<br>`;
debugHTML += `<strong>🎯 Derived isAuthenticated():</strong> ${!!authState}<br>`;
debugHTML += `<strong>🎯 Derived getUserInfo():</strong> ${!!authState}<br>`;
debugHTML += `<strong>window.nostr exists:</strong> ${!!window.nostr} (${window.nostr?.constructor?.name})<br>`;
debugHTML += `<strong>window.nostr.authState (getter):</strong> ${!!window.nostr?.authState}<br>`;
debugHTML += `<strong>FloatingTab queries getAuthState():</strong> ${!!floatingTab?._getAuthState()}<br>`;
debugHTML += `<strong>SessionStorage 'nostr_login_lite_auth':</strong> ${!!sessionAuthData}<br>`;
debugHTML += `<strong>LocalStorage 'nostr_login_lite_auth':</strong> ${!!localAuthData}<br>`;
debugHTML += `<strong>Session storage nl_ keys:</strong> ${sessionKeys.length}<br>`;
debugHTML += `<strong>Instance hasExtension:</strong> ${instance?.hasExtension}<br>`;
debugHTML += `<strong>Facade installed:</strong> ${instance?.facadeInstalled}<br>`;
debugHTML += '</div>';
debugHTML += '<p><strong>Check the browser console for detailed debug output.</strong></p>';
debugHTML += '<p><strong>NEW Architecture:</strong> Global functions query localStorage/sessionStorage directly as single source of truth</p>';
// Check for consistency issues
const derivedAuth = !!authState;
const floatingTabAuth = !!floatingTab?._getAuthState();
if (floatingTabAuth !== derivedAuth) {
debugHTML += '<p style="color: red;"><strong>🚨 MISMATCH DETECTED:</strong> FloatingTab and global getAuthState() disagree!</p>';
debugHTML += '<p>Both should query the same storage - check implementation.</p>';
} else if (sessionAuthData && !derivedAuth) {
debugHTML += '<p style="color: orange;"><strong>⚠️ PARSING ISSUE:</strong> Session data exists but getAuthState() returns null!</p>';
debugHTML += '<p>Check getAuthState() function - it may not be parsing the stored data correctly.</p>';
} else if (!sessionAuthData && !localAuthData && derivedAuth) {
debugHTML += '<p style="color: orange;"><strong>⚠️ STORAGE ISSUE:</strong> No storage data but getAuthState() returns data!</p>';
debugHTML += '<p>getAuthState() may be reading from unexpected sources.</p>';
}
document.getElementById('results').innerHTML = debugHTML;
}
async function checkStatus() {
try {
console.log('🔍 Checking authentication status using GLOBAL functions...');
// Use the single global storage-based authentication state function
const authState = window.NOSTR_LOGIN_LITE.getAuthState();
console.log('🔍 GLOBAL getAuthState():', authState);
console.log('🔍 Derived isAuthenticated():', !!authState);
console.log('🔍 Derived getUserInfo():', authState);
console.log('🔍 window.nostr:', !!window.nostr);
console.log('🔍 window.nostr.constructor:', window.nostr?.constructor?.name);
// Check storage directly for debugging
const storageKey = 'nostr_login_lite_auth';
const sessionAuthData = sessionStorage.getItem(storageKey);
const localAuthData = localStorage.getItem(storageKey);
console.log('🔍 sessionStorage auth data:', !!sessionAuthData);
console.log('🔍 localStorage auth data:', !!localAuthData);
if (authState) {
let pubkey = null;
try {
if (window.nostr) {
pubkey = await window.nostr.getPublicKey();
} else if (authState.pubkey) {
pubkey = authState.pubkey;
}
} catch (err) {
console.warn('Could not get pubkey:', err.message);
pubkey = authState.pubkey;
}
const method = authState.method;
console.log('✅ Authentication detected via GLOBAL functions - method:', method, 'pubkey:', pubkey?.slice(0, 8) + '...');
document.getElementById('auth-status').innerHTML =
`<strong style="color: green;">✅ Authenticated (Session Isolated)</strong>`;
document.getElementById('auth-details').innerHTML =
`<strong>Method:</strong> ${method}<br>
<strong>Public Key:</strong> ${pubkey ? `${pubkey.slice(0, 16)}...${pubkey.slice(-8)}` : 'Available in authState'}<br>
<strong>Storage:</strong> ${sessionAuthData ? 'sessionStorage' : 'localStorage'} (${sessionAuthData ? 'isolated to this tab' : 'shared across tabs'})<br>
<strong>Persistence:</strong> Survives refresh${sessionAuthData ? ', isolated from other tabs' : ', shared with other tabs'}<br>
<strong>Debug:</strong> Global getAuthState() returns valid data`;
} else if (sessionAuthData || localAuthData) {
// We have storage data but getAuthState() returns null
console.log('⚠️ Storage data exists but getAuthState() returns null');
document.getElementById('auth-status').innerHTML =
`<strong style="color: orange;">⚠️ Authentication data found but not parsed</strong>`;
document.getElementById('auth-details').innerHTML =
`<strong>Storage:</strong> ${sessionAuthData ? 'sessionStorage' : 'localStorage'} has authentication data<br>
<strong>Issue:</strong> getAuthState() returns null<br>
<strong>Debug:</strong> Storage data: session=${!!sessionAuthData}, local=${!!localAuthData}<br>
<strong>Solution:</strong> Check getAuthState() function implementation`;
} else {
console.log('❌ No authentication detected via getAuthState()');
document.getElementById('auth-status').innerHTML =
`<strong style="color: red;">❌ Not authenticated</strong>`;
document.getElementById('auth-details').innerHTML =
`<strong>Storage:</strong> sessionStorage (isolated to this tab)<br>
<strong>Status:</strong> Ready for login - will persist on refresh<br>
<strong>Debug:</strong> getAuthState() returns no authentication data`;
}
} catch (error) {
console.error('❌ Error checking status:', error);
document.getElementById('auth-status').innerHTML =
`<strong style="color: orange;">⚠️ Error checking status</strong>`;
document.getElementById('auth-details').innerHTML =
`Error: ${error.message}<br>
<strong>Debug:</strong> Check browser console for details`;
}
}
async function testSigning() {
try {
// Use global authentication state to check if authenticated
const authState = window.NOSTR_LOGIN_LITE.getAuthState();
if (!authState) {
throw new Error('Not authenticated (checked via global getAuthState())');
}
if (!window.nostr) {
throw new Error('window.nostr not available for signing');
}
const event = {
kind: 1,
content: `Test message from ISOLATED session - ${new Date().toISOString()}`,
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
const signedEvent = await window.nostr.signEvent(event);
document.getElementById('results').innerHTML =
`<h4>✅ Signing Test Successful (Session Isolated)</h4>
<p>This signature was created using the storage-based authentication system.</p>
<p><strong>Authentication Method:</strong> getAuthState() confirmed authentication before signing</p>
<pre>${JSON.stringify(signedEvent, null, 2)}</pre>`;
} catch (error) {
document.getElementById('results').innerHTML =
`<h4>❌ Signing Test Failed</h4>
<p style="color: red;">${error.message}</p>
<p><strong>Debug Info:</strong></p>
<ul>
<li>getAuthState(): ${!!window.NOSTR_LOGIN_LITE.getAuthState()}</li>
<li>window.nostr exists: ${!!window.nostr}</li>
<li>Auth method: ${JSON.stringify(window.NOSTR_LOGIN_LITE.getAuthState()?.method || null)}</li>
</ul>`;
}
}
function openNewWindow() {
const newWindow = window.open(
window.location.href,
'_blank',
'width=900,height=700,scrollbars=yes,resizable=yes'
);
if (newWindow) {
document.getElementById('results').innerHTML =
`<h4>🪟 New Window Opened - Independent Session</h4>
<p><strong>Session Isolation Test:</strong></p>
<ol>
<li>The new window starts unauthenticated (independent session)</li>
<li>Login in the new window with a different method or user</li>
<li>Both windows maintain separate authentication states</li>
<li>Refresh either window - authentication persists within that window only</li>
<li>Close a window - its authentication is lost (sessionStorage cleared)</li>
</ol>
<p><strong>Expected Behavior:</strong> Each window/tab has completely independent authentication that persists on refresh but doesn't leak to other windows.</p>`;
} else {
document.getElementById('results').innerHTML =
`<h4>❌ Failed to Open Window</h4>
<p>Please allow popups and try again</p>`;
}
}
function inspectStorage() {
const sessionStorage_keys = [];
// Inspect sessionStorage (our isolated storage)
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('nl_')) {
sessionStorage_keys.push({
key,
value: sessionStorage.getItem(key)
});
}
}
let content = '<h4>📊 Session Storage Inspection</h4>';
content += '<p><strong>Note:</strong> This tab uses sessionStorage for isolation - data here is independent of other tabs/windows.</p>';
content += '<h5>sessionStorage (This tab only):</h5>';
if (sessionStorage_keys.length === 0) {
content += '<p style="color: #666;">No authentication data found in this session</p>';
} else {
content += '<p style="color: green;">✅ Authentication data found (persists on refresh)</p>';
content += '<pre>' + JSON.stringify(sessionStorage_keys, null, 2) + '</pre>';
}
// Show what would be in localStorage if we weren't using isolation
const localStorage_keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('nl_')) {
localStorage_keys.push(key);
}
}
content += '<h5>localStorage (Not used in isolated mode):</h5>';
if (localStorage_keys.length === 0) {
content += '<p style="color: #666;">No NOSTR_LOGIN_LITE data (expected in isolated mode)</p>';
} else {
content += '<p style="color: orange;">⚠️ Found some data - might be from non-isolated sessions</p>';
}
document.getElementById('storage-content').innerHTML = content;
}
function clearStorage() {
// Clear only sessionStorage (our isolated storage)
const sessionKeys = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('nl_')) {
sessionKeys.push(key);
}
}
sessionKeys.forEach(key => sessionStorage.removeItem(key));
document.getElementById('storage-content').innerHTML =
`<h4>🧹 Session Storage Cleared</h4>
<p>Removed ${sessionKeys.length} authentication items from this tab's sessionStorage</p>
<p><strong>Result:</strong> This tab is now logged out, but other tabs are unaffected</p>`;
// Update status
setTimeout(checkStatus, 100);
}
// Listen for authentication events
window.addEventListener('nlMethodSelected', (event) => {
console.log('Authentication successful in isolated session:', event.detail);
setTimeout(checkStatus, 100);
document.getElementById('results').innerHTML =
`<h4>✅ Authentication Successful (Session Isolated)</h4>
<p><strong>Method:</strong> ${event.detail.method}</p>
<p><strong>Storage:</strong> sessionStorage (isolated to this tab)</p>
<p><strong>Persistence:</strong> Will survive refresh, won't affect other tabs</p>
<p><strong>Test:</strong> Open a new tab - it should start unauthenticated</p>`;
});
window.addEventListener('nlLogout', (event) => {
console.log('Logout detected in isolated session:', event.detail);
setTimeout(checkStatus, 100);
document.getElementById('results').innerHTML =
`<h4>👋 Logged Out (Session Isolated)</h4>
<p>Authentication cleared from this tab's sessionStorage only</p>
<p><strong>Result:</strong> Other tabs remain unaffected by this logout</p>`;
});
// Check status on page load (should restore from sessionStorage if available)
window.addEventListener('load', () => {
setTimeout(checkStatus, 500);
// Show persistence message if we're restoring authentication
if (sessionStorage.getItem('nl_auth_state') || sessionStorage.getItem('nl_current')) {
setTimeout(() => {
document.getElementById('results').innerHTML =
`<h4>🔄 Session Restored</h4>
<p>Authentication state restored from sessionStorage on page load</p>
<p><strong>Isolation confirmed:</strong> This tab's login state is independent</p>`;
}, 1000);
}
});
</script>
</body>
</html>

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;
// Wait a brief moment for WindowNostr to process the authentication
setTimeout(async () => {
console.log('🔍 FloatingTab: Checking authentication state from authoritative source...');
console.log('🔍 FloatingTab: Set isAuthenticated to true');
console.log('🔍 FloatingTab: Set userInfo to:', this.userInfo);
const authState = this._getAuthState();
const isAuthenticated = !!authState;
// 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;
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);
// 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');
}
if (this.options.behavior.hideWhenAuthenticated) {
console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true)');
this.hide();
} else {
console.log('🔍 FloatingTab: Updating appearance (hideWhenAuthenticated=false)');
this._updateAppearance();
}
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;
// Wait a brief moment for WindowNostr to process the authentication
setTimeout(async () => {
console.log('🔍 FloatingTab: Checking authentication state from authoritative source...');
console.log('🔍 FloatingTab: Set isAuthenticated to true');
console.log('🔍 FloatingTab: Set userInfo to:', this.userInfo);
const authState = this._getAuthState();
const isAuthenticated = !!authState;
// 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;
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);
// 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');
}
if (this.options.behavior.hideWhenAuthenticated) {
console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true)');
this.hide();
} else {
console.log('🔍 FloatingTab: Updating appearance (hideWhenAuthenticated=false)');
this._updateAppearance();
}
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

413
login_logic.md Normal file
View File

@@ -0,0 +1,413 @@
# NOSTR_LOGIN_LITE - Login Logic Analysis
This document explains the complete login and authentication flow for the NOSTR_LOGIN_LITE library, including how state is maintained upon page refresh.
## System Architecture Overview
The library uses a **modular authentication architecture** with these key components:
1. **FloatingTab** - UI component for login trigger and status display
2. **Modal** - UI component for authentication method selection
3. **NostrLite** - Main library coordinator and facade manager
4. **WindowNostr** - NIP-07 compliant facade for non-extension methods
5. **AuthManager** - Persistent state management with encryption
6. **Extension Bridge** - Browser extension detection and management
## Authentication Flow Diagrams
### Initial Page Load Flow
```
┌─────────────────────┐
│ Page Loads │
└─────────┬───────────┘
┌─────────────────────┐
│ NOSTR_LOGIN_LITE │
│ .init() called │
└─────────┬───────────┘
┌─────────────────────┐ YES ┌─────────────────────┐
│ Real extension │──────────▶│ Extension-First │
│ detected? │ │ Mode: Don't install │
└─────────┬───────────┘ │ facade │
│ NO └─────────────────────┘
┌─────────────────────┐
│ Install WindowNostr │
│ facade for local/ │
│ NIP-46/readonly │
└─────────┬───────────┘
┌─────────────────────┐ YES ┌─────────────────────┐
│ Persistence │──────────▶│ _attemptAuthRestore │
│ enabled? │ │ called │
└─────────┬───────────┘ └─────────┬───────────┘
│ NO │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Initialization │ │ Check storage for │
│ complete │ │ saved auth state │
└─────────────────────┘ └─────────┬───────────┘
┌─────────────────────┐ YES
│ Valid auth state │────────┐
│ found? │ │
└─────────┬───────────┘ │
│ NO │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Show login UI │ │ Restore auth & │
│ (FloatingTab,etc) │ │ dispatch events │
└─────────────────────┘ └─────────────────────┘
```
### User-Initiated Login Flow
```
┌─────────────────────┐ ┌─────────────────────┐
│ User clicks │ │ User clicks │
│ FloatingTab │ │ Login Button │
└─────────┬───────────┘ └─────────┬───────────┘
│ │
▼ ▼
┌─────────────────────┐ │
│ Extension │ │
│ available? │ │
└─────────┬───────────┘ │
│ YES │
▼ │
┌─────────────────────┐ │
│ Auto-try extension │ │
│ authentication │ │
└─────────┬───────────┘ │
│ SUCCESS │
▼ │
┌─────────────────────┐ │
│ Authentication │ │
│ complete │◀──────────────────┘
└─────────────────────┘ │ FAIL OR ALWAYS
┌─────────────────────┐
│ Open Modal with │
│ method selection: │
│ • Extension │
│ • Local Key │
│ • NIP-46 Connect │
│ • Read-only │
│ • OTP/DM │
└─────────┬───────────┘
┌─────────────────────┐
│ User selects method │
│ and completes auth │
└─────────┬───────────┘
┌─────────────────────┐
│ Authentication │
│ complete │
└─────────────────────┘
```
### Authentication Storage & Persistence Flow
```
┌─────────────────────┐
│ Authentication │
│ successful │
└─────────┬───────────┘
┌─────────────────────┐
│ nlMethodSelected │
│ event dispatched │
└─────────┬───────────┘
┌─────────────────────┐ Extension? ┌─────────────────────┐
│ AuthManager. │─────────────────▶│ Store verification │
│ saveAuthState() │ │ data only (no │
└─────────┬───────────┘ │ secrets) │
│ Local Key? └─────────────────────┘
┌─────────────────────┐
│ Encrypt secret key │
│ with session │
│ password + AES-GCM │
└─────────┬───────────┘
│ NIP-46?
┌─────────────────────┐
│ Store connection │
│ parameters (no │
│ secrets) │
└─────────┬───────────┘
│ Read-only?
┌─────────────────────┐
│ Store method only │
│ (no secrets) │
└─────────┬───────────┘
┌─────────────────────┐ isolateSession? ┌─────────────────────┐
│ Choose storage: │─────────YES─────────▶│ sessionStorage │
│ localStorage vs │ │ (per-window) │
│ sessionStorage │◀────────NO───────────┤ │
└─────────┬───────────┘ └─────────────────────┘
┌─────────────────────┐
│ localStorage │
│ (cross-window) │
└─────────────────────┘
```
## Key Decision Points and Logic
### 1. Extension Detection Logic (Line 994-1046)
**Function:** `NostrLite._isRealExtension(obj)`
```javascript
// Conservative extension detection
if (!obj || typeof obj !== 'object') return false;
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') return false;
// Exclude our own classes
const constructorName = obj.constructor?.name;
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') return false;
if (obj === window.NostrTools) return false;
// Look for extension indicators
const extensionIndicators = [
'_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope',
'_requests', '_pubkey', 'name', 'version', 'description'
];
const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop));
const hasExtensionConstructor = constructorName &&
constructorName !== 'Object' &&
constructorName !== 'Function';
return hasIndicators || hasExtensionConstructor;
```
### 2. Facade Installation Decision (Line 942-972)
**Function:** `NostrLite._setupWindowNostrFacade()`
```
Extension detected? ──YES──▶ DON'T install facade
Store reference for persistence
NO
Install WindowNostr facade ──▶ Handle local/NIP-46/readonly methods
```
### 3. FloatingTab Click Behavior (Line 351-369)
**Current UX Inconsistency Issue:**
```javascript
async _handleClick() {
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
this._showUserMenu(); // Show user options
} else {
// INCONSISTENCY: Auto-tries extension instead of opening modal
if (window.nostr && this._isRealExtension(window.nostr)) {
await this._tryExtensionLogin(window.nostr); // Automatic extension attempt
} else {
if (this.modal) {
this.modal.open({ startScreen: 'login' }); // Fallback to modal
}
}
}
}
```
**Comparison with Login Button behavior:**
- Login Button: **Always** opens modal for user choice
- FloatingTab: **Auto-tries extension first**, only shows modal if denied
### 4. Authentication Restoration on Page Refresh
**Two-Path System:**
#### Path 1: Extension Mode (Line 1115-1173)
```javascript
async _attemptExtensionRestore() {
const authManager = new AuthManager({ isolateSession: this.options?.isolateSession });
const storedAuth = await authManager.restoreAuthState();
if (!storedAuth || storedAuth.method !== 'extension') return null;
// Verify extension still works with same pubkey
if (!window.nostr || !this._isRealExtension(window.nostr)) return null;
const currentPubkey = await window.nostr.getPublicKey();
if (currentPubkey !== storedAuth.pubkey) return null;
// Dispatch nlAuthRestored event for UI updates
window.dispatchEvent(new CustomEvent('nlAuthRestored', { detail: extensionAuth }));
}
```
#### Path 2: Non-Extension Mode (Line 1080-1098)
```javascript
// Uses facade's restoreAuthState method
if (this.facadeInstalled && window.nostr?.restoreAuthState) {
const restoredAuth = await window.nostr.restoreAuthState();
if (restoredAuth) {
// Handle NIP-46 reconnection if needed
if (restoredAuth.requiresReconnection) {
this._showReconnectionPrompt(restoredAuth);
}
}
}
```
### 5. Storage Strategy (Line 1408-1414)
**Storage Type Selection:**
```javascript
if (options.isolateSession) {
this.storage = sessionStorage; // Per-window isolation
} else {
this.storage = localStorage; // Cross-window persistence
}
```
### 6. Event-Driven State Synchronization
**Key Events:**
- `nlMethodSelected` - Dispatched when user completes authentication
- `nlAuthRestored` - Dispatched when authentication is restored from storage
- `nlLogout` - Dispatched when user logs out
- `nlReconnectionRequired` - Dispatched when NIP-46 needs reconnection
**Event Listeners:**
- FloatingTab listens to all auth events for UI updates (Line 271-295)
- WindowNostr listens to nlMethodSelected/nlLogout for state management (Line 823-869)
## State Persistence Security Model
### By Authentication Method:
**Extension:**
- ✅ Store: pubkey, verification metadata
- ❌ Never store: extension object, secrets
- 🔒 Security: Minimal data, 1-hour expiry
**Local Key:**
- ✅ Store: encrypted secret key, pubkey
- 🔒 Security: AES-GCM encryption with session-specific password
- 🔑 Session password stored in sessionStorage (cleared on tab close)
**NIP-46:**
- ✅ Store: connection parameters, pubkey
- ❌ Never store: session secrets
- 🔄 Requires: User reconnection on restore
**Read-only:**
- ✅ Store: method type, pubkey
- ❌ No secrets to store
## Current Issues Identified
### UX Inconsistency (THE MAIN ISSUE)
**Problem:** FloatingTab and Login Button have different click behaviors
- **FloatingTab:** Auto-tries extension → Falls back to modal if denied
- **Login Button:** Always opens modal for user choice
**Impact:**
- Confusing user experience
- Inconsistent interaction patterns
- Users don't get consistent choice of authentication method
**Root Cause:** Line 358-367 in FloatingTab._handleClick() method
### Proposed Solutions:
#### Option 1: Make FloatingTab Consistent (Recommended)
```javascript
async _handleClick() {
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
this._showUserMenu();
} else {
// Always open modal - consistent with login button
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
}
```
#### Option 2: Add Configuration Option
```javascript
floatingTab: {
behavior: {
autoTryExtension: false, // Default to consistent behavior
// ... other options
}
}
```
## ⚠️ IMPLEMENTATION STATUS: READY FOR CODE CHANGES
**User Decision:** FloatingTab should behave exactly like login buttons - always open modal for authentication method selection.
**Required Changes:**
1. **File:** `lite/build.js`
2. **Method:** `FloatingTab._handleClick()` (lines 351-369)
3. **Action:** Remove extension auto-detection, always open modal
**Current Code to Replace (lines 358-367):**
```javascript
// 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' });
}
}
```
**Replacement Code:**
```javascript
// Always open login modal (consistent with login buttons)
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
```
**Critical Safety Notes:**
-**DO NOT** change `_checkExistingAuth()` method (lines 299-349) - this handles automatic restoration on page refresh
-**ONLY** change the click handler to remove manual extension detection
- ✅ Authentication restoration will continue to work properly via the separate restoration system
- ✅ Extension detection logic remains intact for other purposes (storage, verification, etc.)
**After Implementation:**
- Rebuild the library with `node lite/build.js`
- Test that both floating tab and login buttons behave identically
- Verify that automatic login restoration on page refresh still works properly
## Important Notes
1. **Extension-First Architecture:** The system never interferes with real browser extensions
2. **Dual Storage Support:** Supports both per-window (sessionStorage) and cross-window (localStorage) persistence
3. **Security-First:** Sensitive data is always encrypted or not stored
4. **Event-Driven:** All components communicate via custom events
5. **Automatic Restoration:** Authentication state is automatically restored on page refresh when possible
The login logic is complex due to supporting multiple authentication methods, security requirements, and different storage strategies, but it provides a flexible and secure authentication system for Nostr applications.

View File

@@ -9,7 +9,7 @@
--nl-primary-color: #000000;
--nl-secondary-color: #ffffff;
--nl-accent-color: #ff0000;
--nl-muted-color: #666666;
--nl-muted-color: #CCCCCC;
--nl-font-family: "Courier New", Courier, monospace;
--nl-border-radius: 15px;
--nl-border-width: 3px;