18 Commits

Author SHA1 Message Date
Your Name
a79277f3ed small stuff 2025-10-01 10:18:10 -04:00
Your Name
521693cfa1 . 2025-09-24 10:50:11 -04:00
Your Name
3109a93163 Fixed issue not recognizing browser extension 2025-09-22 15:37:53 -04:00
Your Name
4505167246 Change key entry 2025-09-21 11:51:33 -04:00
Your Name
ea387c0c9f Add automated versioning and deployment system 2025-09-21 11:22:26 -04:00
Your Name
a7dceb1156 Fixed persistance issues 2025-09-20 15:33:14 -04:00
Your Name
966d9d0456 documentation changes 2025-09-20 11:08:45 -04:00
Your Name
ccff136edb Single Source of Truth Architecture - Complete authentication state management with storage-based getAuthState() as sole authoritative source 2025-09-20 10:39:43 -04:00
Your Name
8f34c2de73 Seem to have most everything working well. Got persistant state after page refresh, and implmented logout call 2025-09-19 16:09:05 -04:00
Your Name
ca75df8bb4 Fixed issue with bunker. Made the modal more beautiful. 2025-09-19 12:24:13 -04:00
Your Name
c747f1f315 . 2025-09-18 10:18:32 -04:00
Your Name
2a66b5eeec Fixed name display 2025-09-16 18:13:01 -04:00
Your Name
fa9688b17e Implement logging in via seed phrase 2025-09-16 15:51:08 -04:00
Your Name
a0e18c34d6 Add comprehensive sign.html test and update documentation
- Add examples/sign.html with comprehensive extension compatibility testing
- Update README.md with profile fetching API documentation
- Update .gitignore for better file management
- Update examples/button.html and examples/modal.html with latest features

This completes the single-extension architecture implementation with:
- nos2x compatibility through true single-extension mode
- Method switching between extension/local/NIP-46/readonly
- Enhanced profile fetching for floating tab
- Comprehensive debugging and testing capabilities
2025-09-16 12:40:15 -04:00
Your Name
995c3f526c Removed interference with extensions. Had to go back to only allowing handling single extension. 2025-09-16 11:55:47 -04:00
Your Name
77ea4a8e67 cleaned up visuals 2025-09-15 14:52:21 -04:00
Your Name
12d4810f4c login with exposed api for web page fixed. 2025-09-15 14:24:14 -04:00
Your Name
517974699d browser extension signing fixed 2025-09-15 13:51:41 -04:00
20 changed files with 11577 additions and 1916 deletions

11
.gitignore vendored
View File

@@ -1,6 +1,6 @@
# Dependencies # Dependencies
node_modules/ node_modules/
nostr-tools/
# IDE and OS files # IDE and OS files
.idea/ .idea/
@@ -18,10 +18,5 @@ Thumbs.db
log.txt log.txt
Trash/ Trash/
# Environment files nostr-login/
.env nostr-tools/
# Aider files
.aider.chat.history.md
.aider.input.history
.aider.tags.cache.v3/

130
README.md
View File

@@ -1,62 +1,86 @@
Nostr_Login_Lite Nostr_Login_Lite
=========== ===========
## Floating Tab API ## API
Configure persistent floating tab for login/logout: Complete configuration showing all available options:
```javascript ```javascript
await NOSTR_LOGIN_LITE.init({ await window.NOSTR_LOGIN_LITE.init({
// Set the initial theme (default: 'default') // Theme configuration
theme: 'dark', // Choose from 'default' or 'dark' theme: 'default', // 'default' | 'dark' | custom theme name
// Standard configuration options // 🔐 Authentication persistence configuration
persistence: true, // Enable persistent authentication (default: true)
isolateSession: false, // Use sessionStorage for per-tab isolation (default: false = localStorage)
// Relay configuration
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
// Authentication methods
methods: { methods: {
extension: true, extension: true, // Browser extensions (Alby, nos2x, etc.)
local: true, local: true, // Manual key entry & generation
readonly: true, readonly: true, // Read-only mode (no signing)
connect: true, connect: true, // NIP-46 remote signers
otp: true otp: false // OTP/DM authentication (not implemented yet)
}, },
// Floating tab configuration (now uses theme-aware text icons) // Floating tab configuration
floatingTab: { floatingTab: {
enabled: true, enabled: true, // Show/hide floating login tab
hPosition: 0.95, // 0.0-1.0 or '95%' from left hPosition: 0.95, // 0.0 = left edge, 1.0 = right edge
vPosition: 0.5, // 0.0-1.0 or '50%' from top vPosition: 0.1, // 0.0 = top edge, 1.0 = bottom edge
offset: { x: 0, y: 0 }, // Fine-tune positioning (pixels)
appearance: { appearance: {
style: 'pill', // 'pill', 'square', 'circle', 'minimal' style: 'pill', // 'pill' | 'square' | 'circle'
theme: 'auto', // 'auto' follows main theme theme: 'auto', // 'auto' | 'light' | 'dark'
icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET] icon: '[LOGIN]', // Text-based icon
text: 'Login' text: 'Sign In', // Button text
iconOnly: false // Show icon only (no text)
}, },
behavior: { behavior: {
hideWhenAuthenticated: false, hideWhenAuthenticated: false, // Keep visible after login
showUserInfo: true, showUserInfo: true, // Show user info when authenticated
autoSlide: true autoSlide: true, // Slide animation on hover
}, persistent: false // Persist across page reloads
animation: {
slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down'
} }
} }
}); });
// After initialization, you can switch themes dynamically: // Control Methods
NOSTR_LOGIN_LITE.switchTheme('dark'); NOSTR_LOGIN_LITE.launch(); // Open login modal
NOSTR_LOGIN_LITE.switchTheme('default'); NOSTR_LOGIN_LITE.logout(); // Clear authentication state
NOSTR_LOGIN_LITE.switchTheme('dark'); // Change theme
// Or customize individual theme variables: NOSTR_LOGIN_LITE.showFloatingTab(); // Show floating tab
NOSTR_LOGIN_LITE.setThemeVariable('--nl-accent-color', '#00ff00'); NOSTR_LOGIN_LITE.hideFloatingTab(); // Hide floating tab
NOSTR_LOGIN_LITE.updateFloatingTab(options); // Update floating tab options
NOSTR_LOGIN_LITE.toggleFloatingTab(); // Toggle floating tab visibility
// Get Authentication State (Single Source of Truth)
const authState = NOSTR_LOGIN_LITE.getAuthState();
const isAuthenticated = !!authState;
const userInfo = authState; // Contains { method, pubkey, etc. }
``` ```
Control methods: **Authentication Persistence:**
```javascript
NOSTR_LOGIN_LITE.showFloatingTab(); Two-tier configuration system:
NOSTR_LOGIN_LITE.hideFloatingTab();
NOSTR_LOGIN_LITE.updateFloatingTab(options); 1. **`persistence: boolean`** - Master switch for authentication persistence
NOSTR_LOGIN_LITE.destroyFloatingTab(); - `true` (default): Save authentication state for automatic restore
``` - `false`: No persistence - user must login fresh every time
2. **`isolateSession: boolean`** - Storage location when persistence is enabled
- `false` (default): Use localStorage - shared across tabs/windows
- `true`: Use sessionStorage - isolated per tab/window
**Use Cases for Session Isolation (`isolateSession: true`):**
- Multi-tenant applications where different tabs need different users
- Testing environments requiring separate authentication per tab
- Privacy-focused applications that shouldn't share login state across tabs
## Embedded Modal API ## Embedded Modal API
@@ -81,3 +105,31 @@ const modal = NOSTR_LOGIN_LITE.embed('#login-container', {
``` ```
Container can be CSS selector or DOM element. Modal renders inline without backdrop overlay. 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).

3
deploy.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
rsync -avz --chmod=644 --progress lite/{nostr-lite.js,nostr.bundle.js} ubuntu@laantungir.net:WWW/nostr-login-lite/

View File

@@ -35,7 +35,7 @@
} }
#login-button:hover { #login-button:hover {
background: #0052a3; opacity: 0.8;
} }
</style> </style>
</head> </head>
@@ -49,9 +49,12 @@
<script src="../lite/nostr-lite.js"></script> <script src="../lite/nostr-lite.js"></script>
<script> <script>
let isAuthenticated = false;
let currentUser = null;
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({ await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: { methods: {
extension: true, extension: true,
local: true, local: true,
@@ -65,10 +68,66 @@
} }
}); });
document.getElementById('login-button').addEventListener('click', () => { // Listen for authentication events
window.NOSTR_LOGIN_LITE.launch('login'); 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> </script>
</body> </body>

View File

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

252
examples/keytest.html Normal file
View File

@@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedded NOSTR_LOGIN_LITE</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 40px;
background: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 90vh;
}
.container {
max-width: 400px;
width: 100%;
}
#login-container {
/* No styling - let embedded modal blend seamlessly */
}
</style>
</head>
<body>
<div class="container">
<div id="login-container">
<!-- Login interface will appear here -->
</div>
<div id="test-section" style="display: none; margin-top: 30px;">
<h2>Nostr Testing Interface</h2>
<div id="status" style="margin-bottom: 20px; padding: 10px; background: #f0f0f0; border-radius: 5px;"></div>
<div style="display: grid; gap: 15px;">
<button id="sign-button" style="padding: 12px; font-size: 16px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;">
Sign Event
</button>
<button id="nip04-encrypt-button" style="padding: 12px; font-size: 16px; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;">
NIP-04 Encrypt
</button>
<button id="nip04-decrypt-button" style="padding: 12px; font-size: 16px; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;">
NIP-04 Decrypt
</button>
<button id="nip44-encrypt-button" style="padding: 12px; font-size: 16px; background: #6f42c1; color: white; border: none; border-radius: 5px; cursor: pointer;">
NIP-44 Encrypt
</button>
<button id="nip44-decrypt-button" style="padding: 12px; font-size: 16px; background: #6f42c1; color: white; border: none; border-radius: 5px; cursor: pointer;">
NIP-44 Decrypt
</button>
<button id="get-pubkey-button" style="padding: 12px; font-size: 16px; background: #17a2b8; color: white; border: none; border-radius: 5px; cursor: pointer;">
Get Public Key
</button>
</div>
<div id="results" style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 5px; font-family: monospace; white-space: pre-wrap; max-height: 400px; overflow-y: auto;"></div>
</div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,
seedphrase:true,
readonly: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
style: 'square', // 'pill', 'square', 'circle', 'minimal'
// icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
text: 'Login'
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
},
animation: {
slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down'
}
}});
// Check for existing authentication state on page load
const authState = getAuthState();
if (authState && authState.method) {
console.log('Found existing authentication:', authState.method);
document.getElementById('status').textContent = `Authenticated with: ${authState.method}`;
document.getElementById('test-section').style.display = 'block';
// Store some test data for encryption/decryption
window.testCiphertext = null;
window.testCiphertext44 = null;
}
// Listen for authentication events
window.addEventListener('nlMethodSelected', (event) => {
console.log('User authenticated:', event.detail);
document.getElementById('status').textContent = `Authenticated with: ${event.detail.method}`;
document.getElementById('test-section').style.display = 'block';
// Store some test data for encryption/decryption
window.testCiphertext = null;
window.testCiphertext44 = null;
});
window.addEventListener('nlLogout', () => {
console.log('User logged out');
document.getElementById('status').textContent = 'Logged out';
document.getElementById('test-section').style.display = 'none';
document.getElementById('results').innerHTML = '';
});
// Button event listeners
document.getElementById('get-pubkey-button').addEventListener('click', testGetPublicKey);
document.getElementById('sign-button').addEventListener('click', testSigning);
document.getElementById('nip04-encrypt-button').addEventListener('click', testNip04Encrypt);
document.getElementById('nip04-decrypt-button').addEventListener('click', testNip04Decrypt);
document.getElementById('nip44-encrypt-button').addEventListener('click', testNip44Encrypt);
document.getElementById('nip44-decrypt-button').addEventListener('click', testNip44Decrypt);
});
// Test functions
async function testGetPublicKey() {
try {
updateResults('🔑 Getting public key...');
const pubkey = await window.nostr.getPublicKey();
updateResults(`✅ Public Key: ${pubkey}`);
} catch (error) {
updateResults(`❌ Get Public Key Error: ${error.message}`);
}
}
async function testSigning() {
try {
updateResults('✍️ Signing event...');
const event = {
kind: 1,
content: 'Hello from NOSTR_LOGIN_LITE key test! ' + new Date().toISOString(),
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
const signedEvent = await window.nostr.signEvent(event);
updateResults(`✅ Event Signed Successfully:\n${JSON.stringify(signedEvent, null, 2)}`);
} catch (error) {
updateResults(`❌ Sign Event Error: ${error.message}`);
}
}
async function testNip04Encrypt() {
try {
updateResults('🔐 Testing NIP-04 encryption...');
const pubkey = await window.nostr.getPublicKey();
const plaintext = 'Secret message for NIP-04 testing! ' + Date.now();
const ciphertext = await window.nostr.nip04.encrypt(pubkey, plaintext);
window.testCiphertext = ciphertext; // Store for decryption test
updateResults(`✅ NIP-04 Encrypted:\nPlaintext: ${plaintext}\nCiphertext: ${ciphertext}`);
} catch (error) {
updateResults(`❌ NIP-04 Encrypt Error: ${error.message}`);
}
}
async function testNip04Decrypt() {
try {
if (!window.testCiphertext) {
updateResults('❌ No ciphertext available. Run NIP-04 encrypt first.');
return;
}
updateResults('🔓 Testing NIP-04 decryption...');
const pubkey = await window.nostr.getPublicKey();
const decrypted = await window.nostr.nip04.decrypt(pubkey, window.testCiphertext);
updateResults(`✅ NIP-04 Decrypted:\nCiphertext: ${window.testCiphertext}\nDecrypted: ${decrypted}`);
} catch (error) {
updateResults(`❌ NIP-04 Decrypt Error: ${error.message}`);
}
}
async function testNip44Encrypt() {
try {
updateResults('🔐 Testing NIP-44 encryption...');
const pubkey = await window.nostr.getPublicKey();
const plaintext = 'Secret message for NIP-44 testing! ' + Date.now();
const ciphertext = await window.nostr.nip44.encrypt(pubkey, plaintext);
window.testCiphertext44 = ciphertext; // Store for decryption test
updateResults(`✅ NIP-44 Encrypted:\nPlaintext: ${plaintext}\nCiphertext: ${ciphertext}`);
} catch (error) {
updateResults(`❌ NIP-44 Encrypt Error: ${error.message}`);
}
}
async function testNip44Decrypt() {
try {
if (!window.testCiphertext44) {
updateResults('❌ No ciphertext available. Run NIP-44 encrypt first.');
return;
}
updateResults('🔓 Testing NIP-44 decryption...');
const pubkey = await window.nostr.getPublicKey();
const decrypted = await window.nostr.nip44.decrypt(pubkey, window.testCiphertext44);
updateResults(`✅ NIP-44 Decrypted:\nCiphertext: ${window.testCiphertext44}\nDecrypted: ${decrypted}`);
} catch (error) {
updateResults(`❌ NIP-44 Decrypt Error: ${error.message}`);
}
}
function updateResults(message) {
const results = document.getElementById('results');
const timestamp = new Date().toLocaleTimeString();
results.textContent += `[${timestamp}] ${message}\n\n`;
results.scrollTop = results.scrollHeight;
}
</script>
</body>
</html>

View File

@@ -35,6 +35,15 @@
<!-- Load NOSTR_LOGIN_LITE main library (now includes NIP-46 extension) --> <!-- Load NOSTR_LOGIN_LITE main library (now includes NIP-46 extension) -->
<script src="../lite/nostr-lite.js"></script> <script src="../lite/nostr-lite.js"></script>
<!-- Load the official nostr-tools bundle first -->
<!-- <script src="./nostr.bundle.js"></script> -->
<script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script>
<!-- Load NOSTR_LOGIN_LITE main library -->
<script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script>
<!-- <script src="./nostr-lite.js"></script> -->
<script> <script>
@@ -49,21 +58,24 @@
try { try {
await window.NOSTR_LOGIN_LITE.init({ await window.NOSTR_LOGIN_LITE.init({
persistence: true, // Enable persistent authentication (default: true)
isolateSession: true, // Use sessionStorage for per-tab isolation (default: false = localStorage)
theme: 'default', theme: 'default',
darkMode: false, darkMode: false,
relays: [relayUrl, 'wss://relay.damus.io'],
methods: { methods: {
extension: true, extension: true,
local: true, local: true,
readonly: true, seedphrase: true,
connect: true, // Enables "Nostr Connect" (NIP-46) connect: true, // Enables "Nostr Connect" (NIP-46)
remote: true, // Also needed for "Nostr Connect" compatibility remote: true, // Also needed for "Nostr Connect" compatibility
otp: true // Enables "DM/OTP" otp: true // Enables "DM/OTP"
}, },
floatingTab: { floatingTab: {
enabled: true, enabled: true,
hPosition: 0.80, // 95% from left hPosition: .98, // 95% from left
vPosition: 0.01, // 50% from top (center) vPosition: 0, // 50% from top (center)
getUserInfo: true, // Fetch user profiles
getUserRelay: ['wss://relay.laantungir.net'], // Custom relays for profiles
appearance: { appearance: {
style: 'minimal', style: 'minimal',
theme: 'auto', theme: 'auto',
@@ -88,16 +100,17 @@
console.log('SUCCESS', 'NOSTR_LOGIN_LITE initialized successfully'); console.log('SUCCESS', 'NOSTR_LOGIN_LITE initialized successfully');
window.addEventListener('nlMethodSelected', handleAuthEvent); window.addEventListener('nlMethodSelected', handleAuthEvent);
window.addEventListener('nlLogout', handleLogoutEvent);
} catch (error) { } catch (error) {
console.log('ERROR', `Initialization failed: ${error.message}`); console.log('ERROR', `Initialization failed: ${error.message}`);
} }
} }
function handleAuthEvent(event) { function handleAuthEvent(event) {
const {pubkey, method, error } = event.detail; const { pubkey, method, error } = event.detail;
console.log('INFO', `Auth event received: method=${method}`); console.log('INFO', `Auth event received: method=${method}`);
if (method && pubkey) { if (method && pubkey) {
@@ -108,10 +121,20 @@
} else if (error) { } else if (error) {
console.log('ERROR', `Authentication error: ${error}`); console.log('ERROR', `Authentication error: ${error}`);
} }
} }
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 // Load user profile using nostr-tools pool
async function loadUserProfile() { async function loadUserProfile() {
if (!userPubkey) return; if (!userPubkey) return;
@@ -124,7 +147,7 @@
// Create a SimplePool instance // Create a SimplePool instance
const pool = new window.NostrTools.SimplePool(); const pool = new window.NostrTools.SimplePool();
const relays = [relayUrl, 'wss://relay.laantungir.net']; const relays = [relayUrl, 'wss://relay.laantungir.net'];
// Get profile event (kind 0) for the user using querySync // Get profile event (kind 0) for the user using querySync
const events = await pool.querySync(relays, { const events = await pool.querySync(relays, {
kinds: [0], kinds: [0],
@@ -171,7 +194,7 @@
async function logout() { async function logout() {
console.log('INFO', 'Logging out...'); console.log('INFO', 'Logging out...');
try { try {
await nlLite.logout(); window.NOSTR_LOGIN_LITE.logout();
console.log('SUCCESS', 'Logged out successfully'); console.log('SUCCESS', 'Logged out successfully');
} catch (error) { } catch (error) {
console.log('ERROR', `Logout failed: ${error.message}`); console.log('ERROR', `Logout failed: ${error.message}`);

View File

@@ -37,10 +37,11 @@
<script> <script>
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({ await window.NOSTR_LOGIN_LITE.init({
theme: 'dark', theme: 'default',
methods: { methods: {
extension: true, extension: true,
local: true, local: true,
seedphrase:true,
readonly: true, readonly: true,
connect: true, connect: true,
remote: true, remote: true,
@@ -48,12 +49,11 @@
}, },
floatingTab: { floatingTab: {
enabled: true, enabled: true,
hPosition: 0.7, // 0.0-1.0 or '95%' from left hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0.5, // 0.0-1.0 or '50%' from top vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: { appearance: {
style: 'pill', // 'pill', 'square', 'circle', 'minimal' style: 'square', // 'pill', 'square', 'circle', 'minimal'
theme: 'auto', // 'auto' follows main theme // icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
text: 'Login' text: 'Login'
}, },
behavior: { behavior: {

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>

184
examples/sign.html Normal file
View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIP-07 Signing Test</title>
</head>
<body>
<div>
<div id="status"></div>
<div id="test-section" style="display:none;">
<button id="sign-button">Sign Event</button>
<button id="encrypt-button">Test NIP-04 Encrypt</button>
<button id="decrypt-button">Test NIP-04 Decrypt</button>
<div id="results"></div>
</div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
let testPubkey = 'npub1damus9dqe7g7jqn45kjcjgsv0vxjqnk8cxjkf8gqjwm8t8qjm7cqm3z7l';
let ciphertext = '';
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,
readonly: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
icon: '', // Clean display without icon placeholders
text: 'Login'
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
},
getUserInfo: true, // Enable profile fetching
getUserRelay: [ // Specific relays for profile fetching
'wss://relay.laantungir.net'
]
}});
// document.getElementById('login-button').addEventListener('click', () => {
// window.NOSTR_LOGIN_LITE.launch('login');
// });
window.addEventListener('nlMethodSelected', (event) => {
document.getElementById('status').textContent = `Authenticated with: ${event.detail.method}`;
document.getElementById('test-section').style.display = 'block';
});
document.getElementById('sign-button').addEventListener('click', testSigning);
document.getElementById('encrypt-button').addEventListener('click', testEncryption);
document.getElementById('decrypt-button').addEventListener('click', testDecryption);
});
async function testSigning() {
try {
console.log('=== DEBUGGING SIGN EVENT START ===');
console.log('testSigning: window.nostr is:', window.nostr);
console.log('testSigning: window.nostr constructor:', window.nostr?.constructor?.name);
console.log('testSigning: window.nostr === our facade?', window.nostr?.constructor?.name === 'WindowNostr');
// Get user public key for comparison
const userPubkey = await window.nostr.getPublicKey();
console.log('User public key:', userPubkey);
// Check auth state if our facade
if (window.nostr?.constructor?.name === 'WindowNostr') {
console.log('WindowNostr authState:', window.nostr.authState);
console.log('WindowNostr authenticatedExtension:', window.nostr.authenticatedExtension);
console.log('WindowNostr existingNostr:', window.nostr.existingNostr);
}
const event = {
kind: 1,
content: 'Hello from NIP-07!',
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
console.log('=== EVENT BEING SENT TO EXTENSION ===');
console.log('Event object:', JSON.stringify(event, null, 2));
console.log('Event keys:', Object.keys(event));
console.log('Event kind type:', typeof event.kind, event.kind);
console.log('Event content type:', typeof event.content, event.content);
console.log('Event tags type:', typeof event.tags, event.tags);
console.log('Event created_at type:', typeof event.created_at, event.created_at);
console.log('Event created_at value:', event.created_at);
// Check if created_at is within reasonable bounds
const now = Math.floor(Date.now() / 1000);
const timeDiff = Math.abs(event.created_at - now);
console.log('Time difference from now (seconds):', timeDiff);
console.log('Event timestamp as Date:', new Date(event.created_at * 1000));
// Additional debugging for user-specific issues
console.log('=== USER-SPECIFIC DEBUG INFO ===');
console.log('User pubkey length:', userPubkey?.length);
console.log('User pubkey format check (hex):', /^[a-fA-F0-9]{64}$/.test(userPubkey));
// Try to get user profile info if available
try {
const profileEvent = {
kinds: [0],
authors: [userPubkey],
limit: 1
};
console.log('Would query profile with filter:', profileEvent);
} catch (profileErr) {
console.log('Profile query setup failed:', profileErr);
}
console.log('=== ABOUT TO CALL EXTENSION SIGN EVENT ===');
const signedEvent = await window.nostr.signEvent(event);
console.log('=== SIGN EVENT SUCCESSFUL ===');
console.log('Signed event:', JSON.stringify(signedEvent, null, 2));
console.log('Signed event keys:', Object.keys(signedEvent));
console.log('Signature present:', !!signedEvent.sig);
console.log('ID present:', !!signedEvent.id);
console.log('Pubkey matches user:', signedEvent.pubkey === userPubkey);
document.getElementById('results').innerHTML = `<h3>Signed Event:</h3><pre>${JSON.stringify(signedEvent, null, 2)}</pre>`;
console.log('=== DEBUGGING SIGN EVENT END ===');
} catch (error) {
console.error('=== SIGN EVENT ERROR ===');
console.error('Error message:', error.message);
console.error('Error stack:', error.stack);
console.error('Error object:', error);
document.getElementById('results').innerHTML = `<h3>Sign Error:</h3><pre>${error.message}</pre><pre>${error.stack}</pre>`;
}
}
async function testEncryption() {
try {
const plaintext = 'Secret message for testing';
const pubkey = await window.nostr.getPublicKey();
ciphertext = await window.nostr.nip04.encrypt(pubkey, plaintext);
document.getElementById('results').innerHTML = `<h3>Encrypted:</h3><pre>${ciphertext}</pre>`;
} catch (error) {
document.getElementById('results').innerHTML = `<h3>Encrypt Error:</h3><pre>${error.message}</pre>`;
}
}
async function testDecryption() {
try {
if (!ciphertext) {
document.getElementById('results').innerHTML = `<h3>Decrypt Error:</h3><pre>No ciphertext available. Run encrypt first.</pre>`;
return;
}
const pubkey = await window.nostr.getPublicKey();
const decrypted = await window.nostr.nip04.decrypt(pubkey, ciphertext);
document.getElementById('results').innerHTML = `<h3>Decrypted:</h3><pre>${decrypted}</pre>`;
} catch (error) {
document.getElementById('results').innerHTML = `<h3>Decrypt Error:</h3><pre>${error.message}</pre>`;
}
}
</script>
</body>
</html>

107
increment_build_push.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/bin/bash
# increment_build_push.sh
# Automates version increment, build, and git operations
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}🔄 Starting increment, build, and push process...${NC}"
# Function to get the latest git tag
get_latest_tag() {
# Get the latest tag that matches the pattern v*.*.*
git tag -l "v*.*.*" | sort -V | tail -n1
}
# Function to increment version
increment_version() {
local version=$1
# Remove 'v' prefix if present
version=${version#v}
# Split version into parts
IFS='.' read -ra VERSION_PARTS <<< "$version"
# Increment the patch version (last digit)
local major=${VERSION_PARTS[0]}
local minor=${VERSION_PARTS[1]}
local patch=${VERSION_PARTS[2]}
patch=$((patch + 1))
echo "$major.$minor.$patch"
}
# Step 1: Get current version
echo -e "${YELLOW}📋 Getting current version...${NC}"
current_tag=$(get_latest_tag)
if [ -z "$current_tag" ]; then
echo -e "${YELLOW}⚠️ No existing version tags found, starting with v0.1.0${NC}"
current_version="0.1.0"
else
echo -e "Current tag: ${current_tag}"
current_version=${current_tag#v}
fi
# Step 2: Increment version
new_version=$(increment_version "$current_version")
new_tag="v$new_version"
echo -e "${GREEN}📈 Incrementing version: $current_version$new_version${NC}"
# Step 2.5: Save version to lite/VERSION file
echo -e "${YELLOW}💾 Saving version to lite/VERSION...${NC}"
echo "$new_version" > lite/VERSION
echo -e "Version saved: ${GREEN}$new_version${NC}"
# Step 2.5: Run build.js
echo -e "${YELLOW}🔧 Running build process...${NC}"
cd lite
node build.js
cd ..
echo -e "${GREEN}✅ Build completed${NC}"
# Step 3: Git add
echo -e "${YELLOW}📦 Adding files to git...${NC}"
git add .
# Step 4: Handle commit message and commit
commit_message=""
if [ $# -eq 0 ]; then
# No arguments provided, ask for commit message
echo -e "${YELLOW}💬 Please enter a commit message:${NC}"
read -p "> " commit_message
if [ -z "$commit_message" ]; then
echo -e "${RED}❌ Commit message cannot be empty${NC}"
exit 1
fi
else
# Use provided arguments as commit message
commit_message="$*"
fi
echo -e "${YELLOW}💬 Committing changes...${NC}"
git commit -m "$commit_message"
echo -e "${YELLOW}🏷️ Creating git tag: $new_tag${NC}"
git tag "$new_tag"
# Step 5: Git push
echo -e "${YELLOW}🚀 Pushing to remote...${NC}"
git push
git push --tags
echo -e "${GREEN}🎉 Successfully completed:${NC}"
echo -e " • Version incremented to: ${GREEN}$new_version${NC}"
echo -e " • VERSION file updated: ${GREEN}lite/VERSION${NC}"
echo -e " • Build completed: ${GREEN}lite/nostr-lite.js${NC}"
echo -e " • Git tag created: ${GREEN}$new_tag${NC}"
echo -e " • Changes pushed to remote${NC}"
echo -e "\n${GREEN}✨ Process complete!${NC}"

1
lite/VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.7

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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.

1
nostr-tools Submodule

Submodule nostr-tools added at 23aebbd341

View File

@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"liveServer.settings.port": 5501
}
}

View File

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