Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4505167246 | ||
|
|
ea387c0c9f | ||
|
|
a7dceb1156 |
3
deploy.sh
Executable file
3
deploy.sh
Executable 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/
|
||||
240
examples/keytest.html
Normal file
240
examples/keytest.html
Normal file
@@ -0,0 +1,240 @@
|
||||
<!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'
|
||||
}
|
||||
|
||||
}});
|
||||
|
||||
// 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>
|
||||
@@ -49,6 +49,8 @@
|
||||
|
||||
try {
|
||||
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',
|
||||
darkMode: false,
|
||||
methods: {
|
||||
|
||||
107
increment_build_push.sh
Executable file
107
increment_build_push.sh
Executable 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
1
lite/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
0.1.4
|
||||
165
lite/build.js
165
lite/build.js
@@ -131,6 +131,25 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
let modalContent = fs.readFileSync(modalPath, 'utf8');
|
||||
|
||||
// Read version from VERSION file and inject into modal title
|
||||
const versionPath = path.join(__dirname, 'VERSION');
|
||||
let versionTitle = 'Nostr Login';
|
||||
|
||||
if (fs.existsSync(versionPath)) {
|
||||
try {
|
||||
const version = fs.readFileSync(versionPath, 'utf8').trim();
|
||||
versionTitle = `Nostr Login v${version}`;
|
||||
console.log(`🔢 Using version: ${version}`);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not read VERSION file, using default title');
|
||||
}
|
||||
} else {
|
||||
console.log('📋 No VERSION file found, using default title');
|
||||
}
|
||||
|
||||
// Replace the modal title in the content
|
||||
modalContent = modalContent.replace(/modalTitle\.textContent = 'Nostr Login';/, `modalTitle.textContent = '${versionTitle}';`);
|
||||
|
||||
// Skip header comments
|
||||
let lines = modalContent.split('\n');
|
||||
let contentStartIndex = 0;
|
||||
@@ -528,10 +547,21 @@ class FloatingTab {
|
||||
}
|
||||
menu.style.top = tabRect.top + 'px';
|
||||
|
||||
// Menu content
|
||||
const userDisplay = this.userInfo?.pubkey ?
|
||||
\`\${this.userInfo.pubkey.slice(0, 8)}...\${this.userInfo.pubkey.slice(-4)}\` :
|
||||
'Authenticated';
|
||||
// Menu content - use _getAuthState() as single source of truth
|
||||
const authState = this._getAuthState();
|
||||
let userDisplay;
|
||||
|
||||
if (authState?.pubkey) {
|
||||
// Use profile name if available, otherwise pubkey
|
||||
if (this.userProfile?.name || this.userProfile?.display_name) {
|
||||
const userName = this.userProfile.name || this.userProfile.display_name;
|
||||
userDisplay = userName.length > 16 ? \`\${userName.slice(0, 16)}...\` : userName;
|
||||
} else {
|
||||
userDisplay = \`\${authState.pubkey.slice(0, 8)}...\${authState.pubkey.slice(-4)}\`;
|
||||
}
|
||||
} else {
|
||||
userDisplay = 'Authenticated';
|
||||
}
|
||||
|
||||
menu.innerHTML = \`
|
||||
<div style="margin-bottom: 8px; font-weight: bold; color: var(--nl-primary-color);">\${userDisplay}</div>
|
||||
@@ -1163,7 +1193,11 @@ class NostrLite {
|
||||
localStorage.removeItem('nl_current');
|
||||
}
|
||||
|
||||
// Dispatch logout event (AuthManager will clear its own data via WindowNostr listener)
|
||||
// Clear current authentication state directly from storage
|
||||
// This works for ALL methods including extensions (fixes the bug)
|
||||
clearAuthState();
|
||||
|
||||
// Dispatch logout event for UI updates
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('nlLogout', {
|
||||
detail: { timestamp: Date.now() }
|
||||
@@ -1796,12 +1830,12 @@ class WindowNostr {
|
||||
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
|
||||
}
|
||||
|
||||
// Save authentication state for persistence
|
||||
// Use global setAuthState function for unified persistence
|
||||
try {
|
||||
await this.authManager.saveAuthState(event.detail);
|
||||
console.log('WindowNostr: Auth state saved for persistence');
|
||||
setAuthState(event.detail, { isolateSession: this.nostrLite.options?.isolateSession });
|
||||
console.log('WindowNostr: Auth state saved via global setAuthState');
|
||||
} catch (error) {
|
||||
console.error('WindowNostr: Failed to save auth state:', error);
|
||||
console.error('WindowNostr: Failed to save auth state via global setAuthState:', error);
|
||||
}
|
||||
|
||||
// EXTENSION-FIRST: Only reinstall facade for non-extension methods
|
||||
@@ -2210,6 +2244,117 @@ function getAuthState() {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// Global Authentication State Management - Unified Persistence
|
||||
// ======================================
|
||||
|
||||
// Global setAuthState function for unified persistence across all authentication methods
|
||||
function setAuthState(authData, options = {}) {
|
||||
try {
|
||||
console.log('🔍 setAuthState: === GLOBAL AUTH STATE SAVE ===');
|
||||
console.log('🔍 setAuthState: authData:', authData);
|
||||
console.log('🔍 setAuthState: options:', options);
|
||||
|
||||
const storageKey = 'nostr_login_lite_auth';
|
||||
|
||||
// Determine which storage to use based on isolateSession option
|
||||
const storage = options.isolateSession ? sessionStorage : localStorage;
|
||||
const storageType = options.isolateSession ? 'sessionStorage' : 'localStorage';
|
||||
|
||||
console.log('🔍 setAuthState: Using', storageType, 'for persistence');
|
||||
|
||||
// Create auth state object
|
||||
const authState = {
|
||||
method: authData.method,
|
||||
timestamp: Date.now(),
|
||||
pubkey: authData.pubkey
|
||||
};
|
||||
|
||||
// Add method-specific data (but no secrets for extension method)
|
||||
switch (authData.method) {
|
||||
case 'extension':
|
||||
// For extensions, only store verification data - no secrets
|
||||
authState.extensionVerification = {
|
||||
constructor: authData.extension?.constructor?.name,
|
||||
hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function',
|
||||
hasSignEvent: typeof authData.extension?.signEvent === 'function'
|
||||
};
|
||||
console.log('🔍 setAuthState: Extension method - storing verification data only');
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
// For local keys, store the secret (will be encrypted by AuthManager if needed)
|
||||
if (authData.secret) {
|
||||
authState.secret = authData.secret;
|
||||
console.log('🔍 setAuthState: Local method - storing secret key');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'nip46':
|
||||
// For NIP-46, store connection parameters
|
||||
if (authData.signer) {
|
||||
authState.nip46 = {
|
||||
remotePubkey: authData.signer.remotePubkey,
|
||||
relays: authData.signer.relays,
|
||||
// Don't store secret - user will need to reconnect
|
||||
};
|
||||
console.log('🔍 setAuthState: NIP-46 method - storing connection parameters');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'readonly':
|
||||
// Read-only mode has no additional data to store
|
||||
console.log('🔍 setAuthState: Read-only method - storing basic auth state');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('🔍 setAuthState: Unknown auth method:', authData.method);
|
||||
break;
|
||||
}
|
||||
|
||||
// Store the auth state
|
||||
storage.setItem(storageKey, JSON.stringify(authState));
|
||||
console.log('🔍 setAuthState: ✅ Auth state saved successfully');
|
||||
console.log('🔍 setAuthState: Final auth state:', authState);
|
||||
|
||||
return authState;
|
||||
|
||||
} catch (error) {
|
||||
console.error('🔍 setAuthState: ❌ Error saving auth state:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// Global Authentication State Clearing
|
||||
// ======================================
|
||||
|
||||
// Global clearAuthState function for unified auth state clearing
|
||||
function clearAuthState() {
|
||||
try {
|
||||
console.log('🔍 clearAuthState: === GLOBAL AUTH STATE CLEAR ===');
|
||||
|
||||
const storageKey = 'nostr_login_lite_auth';
|
||||
|
||||
// Clear from both storage types to ensure complete cleanup
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
sessionStorage.removeItem(storageKey);
|
||||
sessionStorage.removeItem('nostr_session_key');
|
||||
console.log('🔍 clearAuthState: ✅ Cleared auth state from sessionStorage');
|
||||
}
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(storageKey);
|
||||
console.log('🔍 clearAuthState: ✅ Cleared auth state from localStorage');
|
||||
}
|
||||
|
||||
console.log('🔍 clearAuthState: ✅ All auth state cleared successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('🔍 clearAuthState: ❌ Error clearing auth state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Initialize and export
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -2238,6 +2383,8 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
// GLOBAL AUTHENTICATION STATE API - Single Source of Truth
|
||||
getAuthState: getAuthState,
|
||||
setAuthState: setAuthState,
|
||||
clearAuthState: clearAuthState,
|
||||
|
||||
// Expose for debugging
|
||||
_extensionBridge: nostrLite.extensionBridge,
|
||||
|
||||
@@ -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-20T14:23:53.897Z
|
||||
* Generated on: 2025-09-21T15:51:33.328Z
|
||||
*/
|
||||
|
||||
// Verify dependencies are loaded
|
||||
@@ -381,7 +381,7 @@ class Modal {
|
||||
`;
|
||||
|
||||
const modalTitle = document.createElement('h2');
|
||||
modalTitle.textContent = 'Nostr Login';
|
||||
modalTitle.textContent = 'Nostr Login v0.1.4';
|
||||
modalTitle.style.cssText = `
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
@@ -997,62 +997,8 @@ class Modal {
|
||||
_showLocalKeyScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Local Key';
|
||||
title.style.cssText = 'margin: 0 0 20px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const createButton = document.createElement('button');
|
||||
createButton.textContent = 'Create New Key';
|
||||
createButton.onclick = () => this._createLocalKey();
|
||||
createButton.style.cssText = this._getButtonStyle();
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Existing Key';
|
||||
importButton.onclick = () => this._showImportKeyForm();
|
||||
importButton.style.cssText = this._getButtonStyle() + 'margin-top: 12px;';
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = `
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(createButton);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_createLocalKey() {
|
||||
try {
|
||||
const sk = window.NostrTools.generateSecretKey();
|
||||
const pk = window.NostrTools.getPublicKey(sk);
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||||
const npub = window.NostrTools.nip19.npubEncode(pk);
|
||||
|
||||
this._showKeyDisplay(pk, nsec, 'created');
|
||||
} catch (error) {
|
||||
this._showError('Failed to create key: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
_showImportKeyForm() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Import Local Key';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Enter your secret key in either nsec or hex format:';
|
||||
description.innerHTML = 'Enter your secret key in nsec or hex format, or <span id="generate-new" style="text-decoration: underline; cursor: pointer; color: var(--nl-primary-color);">generate new</span>.';
|
||||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
@@ -1074,10 +1020,40 @@ class Modal {
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Key';
|
||||
importButton.disabled = true;
|
||||
importButton.onclick = () => {
|
||||
if (!importButton.disabled) {
|
||||
this._importLocalKey(textarea.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
importButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
`;
|
||||
|
||||
textarea.oninput = () => {
|
||||
const value = textarea.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1085,33 +1061,94 @@ class Modal {
|
||||
if (format === 'nsec') {
|
||||
formatHint.textContent = '✅ Valid nsec format detected';
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
importButton.disabled = false;
|
||||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
importButton.style.color = 'var(--nl-primary-color)';
|
||||
importButton.style.cursor = 'pointer';
|
||||
} else if (format === 'hex') {
|
||||
formatHint.textContent = '✅ Valid hex format detected';
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
importButton.disabled = false;
|
||||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
importButton.style.color = 'var(--nl-primary-color)';
|
||||
importButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid key format - must be nsec1... or 64-character hex';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Key';
|
||||
importButton.onclick = () => this._importLocalKey(textarea.value);
|
||||
importButton.style.cssText = this._getButtonStyle();
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._showLocalKeyScreen();
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(textarea);
|
||||
this.modalBody.appendChild(formatHint);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
|
||||
// Add click handler for the "generate new" link
|
||||
const generateLink = document.getElementById('generate-new');
|
||||
if (generateLink) {
|
||||
generateLink.addEventListener('mouseenter', () => {
|
||||
generateLink.style.color = 'var(--nl-accent-color)';
|
||||
});
|
||||
generateLink.addEventListener('mouseleave', () => {
|
||||
generateLink.style.color = 'var(--nl-primary-color)';
|
||||
});
|
||||
generateLink.addEventListener('click', () => {
|
||||
this._generateNewLocalKey(textarea, formatHint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_generateNewLocalKey(textarea, formatHint) {
|
||||
try {
|
||||
// Generate a new secret key using NostrTools
|
||||
const sk = window.NostrTools.generateSecretKey();
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||||
|
||||
// Set the generated key in the textarea
|
||||
textarea.value = nsec;
|
||||
|
||||
// Trigger the oninput event to properly validate and enable the button
|
||||
if (textarea.oninput) {
|
||||
textarea.oninput();
|
||||
}
|
||||
|
||||
console.log('Generated new local secret key (nsec format)');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate local key:', error);
|
||||
formatHint.textContent = '❌ Failed to generate key - NostrTools not available';
|
||||
formatHint.style.color = '#dc2626';
|
||||
}
|
||||
}
|
||||
|
||||
_createLocalKey() {
|
||||
try {
|
||||
const sk = window.NostrTools.generateSecretKey();
|
||||
const pk = window.NostrTools.getPublicKey(sk);
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||||
const npub = window.NostrTools.nip19.npubEncode(pk);
|
||||
|
||||
this._showKeyDisplay(pk, nsec, 'created');
|
||||
} catch (error) {
|
||||
this._showError('Failed to create key: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
_detectKeyFormat(keyValue) {
|
||||
const trimmed = keyValue.trim();
|
||||
|
||||
@@ -1253,24 +1290,28 @@ class Modal {
|
||||
nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const nsecContainer = document.createElement('div');
|
||||
nsecContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||||
nsecContainer.style.cssText = 'margin-bottom: 8px;';
|
||||
|
||||
const nsecCode = document.createElement('code');
|
||||
nsecCode.textContent = nsec;
|
||||
nsecCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const nsecCopyBtn = createCopyButton(nsec, 'nsec');
|
||||
nsecCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
nsecContainer.appendChild(nsecCode);
|
||||
nsecContainer.appendChild(createCopyButton(nsec, 'nsec'));
|
||||
nsecContainer.appendChild(nsecCopyBtn);
|
||||
nsecSection.appendChild(nsecLabel);
|
||||
nsecSection.appendChild(nsecContainer);
|
||||
|
||||
@@ -1281,24 +1322,28 @@ class Modal {
|
||||
secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const secretHexContainer = document.createElement('div');
|
||||
secretHexContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||||
secretHexContainer.style.cssText = 'margin-bottom: 8px;';
|
||||
|
||||
const secretHexCode = document.createElement('code');
|
||||
secretHexCode.textContent = secretKeyHex;
|
||||
secretHexCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const secretHexCopyBtn = createCopyButton(secretKeyHex, 'hex');
|
||||
secretHexCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
secretHexContainer.appendChild(secretHexCode);
|
||||
secretHexContainer.appendChild(createCopyButton(secretKeyHex, 'hex'));
|
||||
secretHexContainer.appendChild(secretHexCopyBtn);
|
||||
nsecSection.appendChild(secretHexLabel);
|
||||
nsecSection.appendChild(secretHexContainer);
|
||||
}
|
||||
@@ -1314,24 +1359,28 @@ class Modal {
|
||||
npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const npubContainer = document.createElement('div');
|
||||
npubContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||||
npubContainer.style.cssText = 'margin-bottom: 8px;';
|
||||
|
||||
const npubCode = document.createElement('code');
|
||||
npubCode.textContent = npub;
|
||||
npubCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const npubCopyBtn = createCopyButton(npub, 'npub');
|
||||
npubCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
npubContainer.appendChild(npubCode);
|
||||
npubContainer.appendChild(createCopyButton(npub, 'npub'));
|
||||
npubContainer.appendChild(npubCopyBtn);
|
||||
npubSection.appendChild(npubLabel);
|
||||
npubSection.appendChild(npubContainer);
|
||||
|
||||
@@ -1341,24 +1390,28 @@ class Modal {
|
||||
pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const pubHexContainer = document.createElement('div');
|
||||
pubHexContainer.style.cssText = 'display: flex; align-items: flex-start;';
|
||||
pubHexContainer.style.cssText = '';
|
||||
|
||||
const pubHexCode = document.createElement('code');
|
||||
pubHexCode.textContent = pubkeyHex;
|
||||
pubHexCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const pubHexCopyBtn = createCopyButton(pubkeyHex, 'hex');
|
||||
pubHexCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
pubHexContainer.appendChild(pubHexCode);
|
||||
pubHexContainer.appendChild(createCopyButton(pubkeyHex, 'hex'));
|
||||
pubHexContainer.appendChild(pubHexCopyBtn);
|
||||
npubSection.appendChild(pubHexLabel);
|
||||
npubSection.appendChild(pubHexContainer);
|
||||
|
||||
@@ -1382,11 +1435,18 @@ class Modal {
|
||||
if (method === 'extension') {
|
||||
console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension');
|
||||
|
||||
// Save extension authentication state using global setAuthState function
|
||||
if (typeof window.setAuthState === 'function') {
|
||||
console.log('Modal: Saving extension auth state to storage');
|
||||
window.setAuthState({ method, ...options }, { isolateSession: this.options?.isolateSession });
|
||||
}
|
||||
|
||||
// Emit auth method selection directly for extension
|
||||
const event = new CustomEvent('nlMethodSelected', {
|
||||
detail: { method, ...options }
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
@@ -2495,10 +2555,21 @@ class FloatingTab {
|
||||
}
|
||||
menu.style.top = tabRect.top + 'px';
|
||||
|
||||
// Menu content
|
||||
const userDisplay = this.userInfo?.pubkey ?
|
||||
`${this.userInfo.pubkey.slice(0, 8)}...${this.userInfo.pubkey.slice(-4)}` :
|
||||
'Authenticated';
|
||||
// Menu content - use _getAuthState() as single source of truth
|
||||
const authState = this._getAuthState();
|
||||
let userDisplay;
|
||||
|
||||
if (authState?.pubkey) {
|
||||
// Use profile name if available, otherwise pubkey
|
||||
if (this.userProfile?.name || this.userProfile?.display_name) {
|
||||
const userName = this.userProfile.name || this.userProfile.display_name;
|
||||
userDisplay = userName.length > 16 ? `${userName.slice(0, 16)}...` : userName;
|
||||
} else {
|
||||
userDisplay = `${authState.pubkey.slice(0, 8)}...${authState.pubkey.slice(-4)}`;
|
||||
}
|
||||
} else {
|
||||
userDisplay = 'Authenticated';
|
||||
}
|
||||
|
||||
menu.innerHTML = `
|
||||
<div style="margin-bottom: 8px; font-weight: bold; color: var(--nl-primary-color);">${userDisplay}</div>
|
||||
@@ -3130,7 +3201,11 @@ class NostrLite {
|
||||
localStorage.removeItem('nl_current');
|
||||
}
|
||||
|
||||
// Dispatch logout event (AuthManager will clear its own data via WindowNostr listener)
|
||||
// Clear current authentication state directly from storage
|
||||
// This works for ALL methods including extensions (fixes the bug)
|
||||
clearAuthState();
|
||||
|
||||
// Dispatch logout event for UI updates
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('nlLogout', {
|
||||
detail: { timestamp: Date.now() }
|
||||
@@ -3763,12 +3838,12 @@ class WindowNostr {
|
||||
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
|
||||
}
|
||||
|
||||
// Save authentication state for persistence
|
||||
// Use global setAuthState function for unified persistence
|
||||
try {
|
||||
await this.authManager.saveAuthState(event.detail);
|
||||
console.log('WindowNostr: Auth state saved for persistence');
|
||||
setAuthState(event.detail, { isolateSession: this.nostrLite.options?.isolateSession });
|
||||
console.log('WindowNostr: Auth state saved via global setAuthState');
|
||||
} catch (error) {
|
||||
console.error('WindowNostr: Failed to save auth state:', error);
|
||||
console.error('WindowNostr: Failed to save auth state via global setAuthState:', error);
|
||||
}
|
||||
|
||||
// EXTENSION-FIRST: Only reinstall facade for non-extension methods
|
||||
@@ -4177,6 +4252,117 @@ function getAuthState() {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// Global Authentication State Management - Unified Persistence
|
||||
// ======================================
|
||||
|
||||
// Global setAuthState function for unified persistence across all authentication methods
|
||||
function setAuthState(authData, options = {}) {
|
||||
try {
|
||||
console.log('🔍 setAuthState: === GLOBAL AUTH STATE SAVE ===');
|
||||
console.log('🔍 setAuthState: authData:', authData);
|
||||
console.log('🔍 setAuthState: options:', options);
|
||||
|
||||
const storageKey = 'nostr_login_lite_auth';
|
||||
|
||||
// Determine which storage to use based on isolateSession option
|
||||
const storage = options.isolateSession ? sessionStorage : localStorage;
|
||||
const storageType = options.isolateSession ? 'sessionStorage' : 'localStorage';
|
||||
|
||||
console.log('🔍 setAuthState: Using', storageType, 'for persistence');
|
||||
|
||||
// Create auth state object
|
||||
const authState = {
|
||||
method: authData.method,
|
||||
timestamp: Date.now(),
|
||||
pubkey: authData.pubkey
|
||||
};
|
||||
|
||||
// Add method-specific data (but no secrets for extension method)
|
||||
switch (authData.method) {
|
||||
case 'extension':
|
||||
// For extensions, only store verification data - no secrets
|
||||
authState.extensionVerification = {
|
||||
constructor: authData.extension?.constructor?.name,
|
||||
hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function',
|
||||
hasSignEvent: typeof authData.extension?.signEvent === 'function'
|
||||
};
|
||||
console.log('🔍 setAuthState: Extension method - storing verification data only');
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
// For local keys, store the secret (will be encrypted by AuthManager if needed)
|
||||
if (authData.secret) {
|
||||
authState.secret = authData.secret;
|
||||
console.log('🔍 setAuthState: Local method - storing secret key');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'nip46':
|
||||
// For NIP-46, store connection parameters
|
||||
if (authData.signer) {
|
||||
authState.nip46 = {
|
||||
remotePubkey: authData.signer.remotePubkey,
|
||||
relays: authData.signer.relays,
|
||||
// Don't store secret - user will need to reconnect
|
||||
};
|
||||
console.log('🔍 setAuthState: NIP-46 method - storing connection parameters');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'readonly':
|
||||
// Read-only mode has no additional data to store
|
||||
console.log('🔍 setAuthState: Read-only method - storing basic auth state');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('🔍 setAuthState: Unknown auth method:', authData.method);
|
||||
break;
|
||||
}
|
||||
|
||||
// Store the auth state
|
||||
storage.setItem(storageKey, JSON.stringify(authState));
|
||||
console.log('🔍 setAuthState: ✅ Auth state saved successfully');
|
||||
console.log('🔍 setAuthState: Final auth state:', authState);
|
||||
|
||||
return authState;
|
||||
|
||||
} catch (error) {
|
||||
console.error('🔍 setAuthState: ❌ Error saving auth state:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// Global Authentication State Clearing
|
||||
// ======================================
|
||||
|
||||
// Global clearAuthState function for unified auth state clearing
|
||||
function clearAuthState() {
|
||||
try {
|
||||
console.log('🔍 clearAuthState: === GLOBAL AUTH STATE CLEAR ===');
|
||||
|
||||
const storageKey = 'nostr_login_lite_auth';
|
||||
|
||||
// Clear from both storage types to ensure complete cleanup
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
sessionStorage.removeItem(storageKey);
|
||||
sessionStorage.removeItem('nostr_session_key');
|
||||
console.log('🔍 clearAuthState: ✅ Cleared auth state from sessionStorage');
|
||||
}
|
||||
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(storageKey);
|
||||
console.log('🔍 clearAuthState: ✅ Cleared auth state from localStorage');
|
||||
}
|
||||
|
||||
console.log('🔍 clearAuthState: ✅ All auth state cleared successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('🔍 clearAuthState: ❌ Error clearing auth state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Initialize and export
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -4205,6 +4391,8 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
// GLOBAL AUTHENTICATION STATE API - Single Source of Truth
|
||||
getAuthState: getAuthState,
|
||||
setAuthState: setAuthState,
|
||||
clearAuthState: clearAuthState,
|
||||
|
||||
// Expose for debugging
|
||||
_extensionBridge: nostrLite.extensionBridge,
|
||||
|
||||
224
lite/ui/modal.js
224
lite/ui/modal.js
@@ -700,62 +700,8 @@ class Modal {
|
||||
_showLocalKeyScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Local Key';
|
||||
title.style.cssText = 'margin: 0 0 20px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const createButton = document.createElement('button');
|
||||
createButton.textContent = 'Create New Key';
|
||||
createButton.onclick = () => this._createLocalKey();
|
||||
createButton.style.cssText = this._getButtonStyle();
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Existing Key';
|
||||
importButton.onclick = () => this._showImportKeyForm();
|
||||
importButton.style.cssText = this._getButtonStyle() + 'margin-top: 12px;';
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = `
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(createButton);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_createLocalKey() {
|
||||
try {
|
||||
const sk = window.NostrTools.generateSecretKey();
|
||||
const pk = window.NostrTools.getPublicKey(sk);
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||||
const npub = window.NostrTools.nip19.npubEncode(pk);
|
||||
|
||||
this._showKeyDisplay(pk, nsec, 'created');
|
||||
} catch (error) {
|
||||
this._showError('Failed to create key: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
_showImportKeyForm() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Import Local Key';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Enter your secret key in either nsec or hex format:';
|
||||
description.innerHTML = 'Enter your secret key in nsec or hex format, or <span id="generate-new" style="text-decoration: underline; cursor: pointer; color: var(--nl-primary-color);">generate new</span>.';
|
||||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
@@ -777,10 +723,40 @@ class Modal {
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Key';
|
||||
importButton.disabled = true;
|
||||
importButton.onclick = () => {
|
||||
if (!importButton.disabled) {
|
||||
this._importLocalKey(textarea.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
importButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
`;
|
||||
|
||||
textarea.oninput = () => {
|
||||
const value = textarea.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -788,33 +764,94 @@ class Modal {
|
||||
if (format === 'nsec') {
|
||||
formatHint.textContent = '✅ Valid nsec format detected';
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
importButton.disabled = false;
|
||||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
importButton.style.color = 'var(--nl-primary-color)';
|
||||
importButton.style.cursor = 'pointer';
|
||||
} else if (format === 'hex') {
|
||||
formatHint.textContent = '✅ Valid hex format detected';
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
importButton.disabled = false;
|
||||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
importButton.style.color = 'var(--nl-primary-color)';
|
||||
importButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid key format - must be nsec1... or 64-character hex';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Key';
|
||||
importButton.onclick = () => this._importLocalKey(textarea.value);
|
||||
importButton.style.cssText = this._getButtonStyle();
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._showLocalKeyScreen();
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(textarea);
|
||||
this.modalBody.appendChild(formatHint);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
|
||||
// Add click handler for the "generate new" link
|
||||
const generateLink = document.getElementById('generate-new');
|
||||
if (generateLink) {
|
||||
generateLink.addEventListener('mouseenter', () => {
|
||||
generateLink.style.color = 'var(--nl-accent-color)';
|
||||
});
|
||||
generateLink.addEventListener('mouseleave', () => {
|
||||
generateLink.style.color = 'var(--nl-primary-color)';
|
||||
});
|
||||
generateLink.addEventListener('click', () => {
|
||||
this._generateNewLocalKey(textarea, formatHint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_generateNewLocalKey(textarea, formatHint) {
|
||||
try {
|
||||
// Generate a new secret key using NostrTools
|
||||
const sk = window.NostrTools.generateSecretKey();
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||||
|
||||
// Set the generated key in the textarea
|
||||
textarea.value = nsec;
|
||||
|
||||
// Trigger the oninput event to properly validate and enable the button
|
||||
if (textarea.oninput) {
|
||||
textarea.oninput();
|
||||
}
|
||||
|
||||
console.log('Generated new local secret key (nsec format)');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate local key:', error);
|
||||
formatHint.textContent = '❌ Failed to generate key - NostrTools not available';
|
||||
formatHint.style.color = '#dc2626';
|
||||
}
|
||||
}
|
||||
|
||||
_createLocalKey() {
|
||||
try {
|
||||
const sk = window.NostrTools.generateSecretKey();
|
||||
const pk = window.NostrTools.getPublicKey(sk);
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||||
const npub = window.NostrTools.nip19.npubEncode(pk);
|
||||
|
||||
this._showKeyDisplay(pk, nsec, 'created');
|
||||
} catch (error) {
|
||||
this._showError('Failed to create key: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
_detectKeyFormat(keyValue) {
|
||||
const trimmed = keyValue.trim();
|
||||
|
||||
@@ -956,24 +993,28 @@ class Modal {
|
||||
nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const nsecContainer = document.createElement('div');
|
||||
nsecContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||||
nsecContainer.style.cssText = 'margin-bottom: 8px;';
|
||||
|
||||
const nsecCode = document.createElement('code');
|
||||
nsecCode.textContent = nsec;
|
||||
nsecCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const nsecCopyBtn = createCopyButton(nsec, 'nsec');
|
||||
nsecCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
nsecContainer.appendChild(nsecCode);
|
||||
nsecContainer.appendChild(createCopyButton(nsec, 'nsec'));
|
||||
nsecContainer.appendChild(nsecCopyBtn);
|
||||
nsecSection.appendChild(nsecLabel);
|
||||
nsecSection.appendChild(nsecContainer);
|
||||
|
||||
@@ -984,24 +1025,28 @@ class Modal {
|
||||
secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const secretHexContainer = document.createElement('div');
|
||||
secretHexContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||||
secretHexContainer.style.cssText = 'margin-bottom: 8px;';
|
||||
|
||||
const secretHexCode = document.createElement('code');
|
||||
secretHexCode.textContent = secretKeyHex;
|
||||
secretHexCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const secretHexCopyBtn = createCopyButton(secretKeyHex, 'hex');
|
||||
secretHexCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
secretHexContainer.appendChild(secretHexCode);
|
||||
secretHexContainer.appendChild(createCopyButton(secretKeyHex, 'hex'));
|
||||
secretHexContainer.appendChild(secretHexCopyBtn);
|
||||
nsecSection.appendChild(secretHexLabel);
|
||||
nsecSection.appendChild(secretHexContainer);
|
||||
}
|
||||
@@ -1017,24 +1062,28 @@ class Modal {
|
||||
npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const npubContainer = document.createElement('div');
|
||||
npubContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||||
npubContainer.style.cssText = 'margin-bottom: 8px;';
|
||||
|
||||
const npubCode = document.createElement('code');
|
||||
npubCode.textContent = npub;
|
||||
npubCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const npubCopyBtn = createCopyButton(npub, 'npub');
|
||||
npubCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
npubContainer.appendChild(npubCode);
|
||||
npubContainer.appendChild(createCopyButton(npub, 'npub'));
|
||||
npubContainer.appendChild(npubCopyBtn);
|
||||
npubSection.appendChild(npubLabel);
|
||||
npubSection.appendChild(npubContainer);
|
||||
|
||||
@@ -1044,24 +1093,28 @@ class Modal {
|
||||
pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||
|
||||
const pubHexContainer = document.createElement('div');
|
||||
pubHexContainer.style.cssText = 'display: flex; align-items: flex-start;';
|
||||
pubHexContainer.style.cssText = '';
|
||||
|
||||
const pubHexCode = document.createElement('code');
|
||||
pubHexCode.textContent = pubkeyHex;
|
||||
pubHexCode.style.cssText = `
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
background: #f3f4f6;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const pubHexCopyBtn = createCopyButton(pubkeyHex, 'hex');
|
||||
pubHexCopyBtn.style.cssText += 'display: inline-block; margin-left: 0;';
|
||||
|
||||
pubHexContainer.appendChild(pubHexCode);
|
||||
pubHexContainer.appendChild(createCopyButton(pubkeyHex, 'hex'));
|
||||
pubHexContainer.appendChild(pubHexCopyBtn);
|
||||
npubSection.appendChild(pubHexLabel);
|
||||
npubSection.appendChild(pubHexContainer);
|
||||
|
||||
@@ -1085,11 +1138,18 @@ class Modal {
|
||||
if (method === 'extension') {
|
||||
console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension');
|
||||
|
||||
// Save extension authentication state using global setAuthState function
|
||||
if (typeof window.setAuthState === 'function') {
|
||||
console.log('Modal: Saving extension auth state to storage');
|
||||
window.setAuthState({ method, ...options }, { isolateSession: this.options?.isolateSession });
|
||||
}
|
||||
|
||||
// Emit auth method selection directly for extension
|
||||
const event = new CustomEvent('nlMethodSelected', {
|
||||
detail: { method, ...options }
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user