Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a5306f86 | ||
|
|
98b87de736 | ||
|
|
ae6f176f52 | ||
|
|
a79277f3ed | ||
|
|
521693cfa1 | ||
|
|
3109a93163 | ||
|
|
4505167246 | ||
|
|
ea387c0c9f | ||
|
|
a7dceb1156 | ||
|
|
966d9d0456 | ||
|
|
ccff136edb | ||
|
|
8f34c2de73 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
nostr-tools/
|
|
||||||
|
|
||||||
# IDE and OS files
|
# IDE and OS files
|
||||||
.idea/
|
.idea/
|
||||||
@@ -18,4 +18,5 @@ Thumbs.db
|
|||||||
log.txt
|
log.txt
|
||||||
Trash/
|
Trash/
|
||||||
|
|
||||||
nostr-login/
|
nostr-login/
|
||||||
|
nostr-tools/
|
||||||
|
|||||||
135
README.md
135
README.md
@@ -1,67 +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
|
||||||
getUserInfo: true, // Fetch user profile name from relays
|
offset: { x: 0, y: 0 }, // Fine-tune positioning (pixels)
|
||||||
getUserRelay: [ // Relays for profile queries
|
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol'
|
|
||||||
],
|
|
||||||
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
|
||||||
|
|
||||||
@@ -86,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).
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3
deploy.sh
Executable file
3
deploy.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rsync -avz --chmod=644 --progress build/{nostr-lite.js,nostr.bundle.js} ubuntu@laantungir.net:html/nostr-login-lite/
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#login-button:hover {
|
#login-button:hover {
|
||||||
background: #0052a3;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -49,6 +49,9 @@
|
|||||||
<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({
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
252
examples/keytest.html
Normal 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>
|
||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
534
examples/session-isolation-test.html
Normal file
534
examples/session-isolation-test.html
Normal 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>
|
||||||
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 src/VERSION file
|
||||||
|
echo -e "${YELLOW}💾 Saving version to src/VERSION...${NC}"
|
||||||
|
echo "$new_version" > src/VERSION
|
||||||
|
echo -e "Version saved: ${GREEN}$new_version${NC}"
|
||||||
|
|
||||||
|
# Step 2.5: Run build.js
|
||||||
|
echo -e "${YELLOW}🔧 Running build process...${NC}"
|
||||||
|
cd src
|
||||||
|
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}src/VERSION${NC}"
|
||||||
|
echo -e " • Build completed: ${GREEN}build/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}"
|
||||||
1361
lite/build.js
1361
lite/build.js
File diff suppressed because it is too large
Load Diff
413
login_logic.md
Normal file
413
login_logic.md
Normal 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.
|
||||||
10
nostr_login_lite.code-workspace
Normal file
10
nostr_login_lite.code-workspace
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"liveServer.settings.port": 5501
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/VERSION
Normal file
1
src/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.1.10
|
||||||
2306
src/build.js
Normal file
2306
src/build.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -330,7 +330,7 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleOptionClick(type) {
|
_handleOptionClick(type) {
|
||||||
console.log('Selected login type:', type);
|
// console.log('Selected login type:', type);
|
||||||
|
|
||||||
// Handle different login types
|
// Handle different login types
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -362,16 +362,16 @@ class Modal {
|
|||||||
// Check if NostrLite instance has a preserved extension (real extension detected at init)
|
// Check if NostrLite instance has a preserved extension (real extension detected at init)
|
||||||
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||||
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||||
console.log('Modal: Using preserved extension:', extension.constructor?.name);
|
// console.log('Modal: Using preserved extension:', extension.constructor?.name);
|
||||||
}
|
}
|
||||||
// Otherwise check current window.nostr
|
// Otherwise check current window.nostr
|
||||||
else if (window.nostr && this._isRealExtension(window.nostr)) {
|
else if (window.nostr && this._isRealExtension(window.nostr)) {
|
||||||
extension = window.nostr;
|
extension = window.nostr;
|
||||||
console.log('Modal: Using current window.nostr extension:', extension.constructor?.name);
|
// console.log('Modal: Using current window.nostr extension:', extension.constructor?.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!extension) {
|
if (!extension) {
|
||||||
console.log('Modal: No extension detected yet, waiting for deferred detection...');
|
// console.log('Modal: No extension detected yet, waiting for deferred detection...');
|
||||||
|
|
||||||
// DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library
|
// DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
@@ -382,24 +382,24 @@ class Modal {
|
|||||||
// Check again for preserved extension (might be set by deferred detection)
|
// Check again for preserved extension (might be set by deferred detection)
|
||||||
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||||
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||||
console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name);
|
// console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name);
|
||||||
this._tryExtensionLogin(extension);
|
this._tryExtensionLogin(extension);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check current window.nostr again
|
// Check current window.nostr again
|
||||||
if (window.nostr && this._isRealExtension(window.nostr)) {
|
if (window.nostr && this._isRealExtension(window.nostr)) {
|
||||||
extension = window.nostr;
|
extension = window.nostr;
|
||||||
console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name);
|
// console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name);
|
||||||
this._tryExtensionLogin(extension);
|
this._tryExtensionLogin(extension);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep trying or give up
|
// Keep trying or give up
|
||||||
if (attempts < maxAttempts) {
|
if (attempts < maxAttempts) {
|
||||||
setTimeout(checkForExtension, 200);
|
setTimeout(checkForExtension, 200);
|
||||||
} else {
|
} else {
|
||||||
console.log('Modal: No browser extension found after waiting 2 seconds');
|
// console.log('Modal: No browser extension found after waiting 2 seconds');
|
||||||
this._showExtensionRequired();
|
this._showExtensionRequired();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -410,7 +410,7 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the single detected extension directly - no choice UI
|
// Use the single detected extension directly - no choice UI
|
||||||
console.log('Modal: Single extension mode - using extension directly');
|
// console.log('Modal: Single extension mode - using extension directly');
|
||||||
this._tryExtensionLogin(extension);
|
this._tryExtensionLogin(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,9 +434,9 @@ class Modal {
|
|||||||
for (const location of locations) {
|
for (const location of locations) {
|
||||||
try {
|
try {
|
||||||
const obj = location.getter();
|
const obj = location.getter();
|
||||||
|
|
||||||
console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name);
|
// console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name);
|
||||||
|
|
||||||
if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) {
|
if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) {
|
||||||
extensions.push({
|
extensions.push({
|
||||||
name: location.name,
|
name: location.name,
|
||||||
@@ -445,26 +445,26 @@ class Modal {
|
|||||||
extension: obj
|
extension: obj
|
||||||
});
|
});
|
||||||
seenExtensions.add(obj);
|
seenExtensions.add(obj);
|
||||||
console.log(`Modal: ✓ Detected extension at ${location.name} (${obj.constructor?.name})`);
|
// console.log(`Modal: ✓ Detected extension at ${location.name} (${obj.constructor?.name})`);
|
||||||
} else if (obj) {
|
} else if (obj) {
|
||||||
console.log(`Modal: ✗ Filtered out ${location.name} (${obj.constructor?.name})`);
|
// console.log(`Modal: ✗ Filtered out ${location.name} (${obj.constructor?.name})`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Location doesn't exist or can't be accessed
|
// Location doesn't exist or can't be accessed
|
||||||
console.log(`Modal: ${location.name} not accessible:`, e.message);
|
// console.log(`Modal: ${location.name} not accessible:`, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check window.nostr but be extra careful to avoid our library
|
// Also check window.nostr but be extra careful to avoid our library
|
||||||
console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name);
|
// console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name);
|
||||||
|
|
||||||
if (window.nostr) {
|
if (window.nostr) {
|
||||||
// Check if window.nostr is our WindowNostr facade with a preserved extension
|
// Check if window.nostr is our WindowNostr facade with a preserved extension
|
||||||
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
|
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
|
||||||
console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
|
// console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
|
||||||
const preservedExtension = window.nostr.existingNostr;
|
const preservedExtension = window.nostr.existingNostr;
|
||||||
console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
|
// console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
|
||||||
|
|
||||||
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
|
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
|
||||||
extensions.push({
|
extensions.push({
|
||||||
name: 'window.nostr.existingNostr',
|
name: 'window.nostr.existingNostr',
|
||||||
@@ -473,7 +473,7 @@ class Modal {
|
|||||||
extension: preservedExtension
|
extension: preservedExtension
|
||||||
});
|
});
|
||||||
seenExtensions.add(preservedExtension);
|
seenExtensions.add(preservedExtension);
|
||||||
console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`);
|
// console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if window.nostr is directly a real extension (not our facade)
|
// Check if window.nostr is directly a real extension (not our facade)
|
||||||
@@ -485,9 +485,9 @@ class Modal {
|
|||||||
extension: window.nostr
|
extension: window.nostr
|
||||||
});
|
});
|
||||||
seenExtensions.add(window.nostr);
|
seenExtensions.add(window.nostr);
|
||||||
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
// console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`);
|
// console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,27 +495,27 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isRealExtension(obj) {
|
_isRealExtension(obj) {
|
||||||
console.log(`Modal: EXTENSIVE DEBUG - _isRealExtension called with:`, obj);
|
// console.log(`Modal: EXTENSIVE DEBUG - _isRealExtension called with:`, obj);
|
||||||
console.log(`Modal: Object type: ${typeof obj}`);
|
// console.log(`Modal: Object type: ${typeof obj}`);
|
||||||
console.log(`Modal: Object truthy: ${!!obj}`);
|
// console.log(`Modal: Object truthy: ${!!obj}`);
|
||||||
|
|
||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
console.log(`Modal: REJECT - Not an object`);
|
// console.log(`Modal: REJECT - Not an object`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Modal: getPublicKey type: ${typeof obj.getPublicKey}`);
|
// console.log(`Modal: getPublicKey type: ${typeof obj.getPublicKey}`);
|
||||||
console.log(`Modal: signEvent type: ${typeof obj.signEvent}`);
|
// console.log(`Modal: signEvent type: ${typeof obj.signEvent}`);
|
||||||
|
|
||||||
// Must have required Nostr methods
|
// Must have required Nostr methods
|
||||||
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
||||||
console.log(`Modal: REJECT - Missing required methods`);
|
// console.log(`Modal: REJECT - Missing required methods`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude NostrTools library object
|
// Exclude NostrTools library object
|
||||||
if (obj === window.NostrTools) {
|
if (obj === window.NostrTools) {
|
||||||
console.log(`Modal: REJECT - Is NostrTools object`);
|
// console.log(`Modal: REJECT - Is NostrTools object`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,10 +523,10 @@ class Modal {
|
|||||||
// This is the key fix - match the comprehensive test's successful detection logic
|
// This is the key fix - match the comprehensive test's successful detection logic
|
||||||
const constructorName = obj.constructor?.name;
|
const constructorName = obj.constructor?.name;
|
||||||
const objectKeys = Object.keys(obj);
|
const objectKeys = Object.keys(obj);
|
||||||
|
|
||||||
console.log(`Modal: Constructor name: "${constructorName}"`);
|
// console.log(`Modal: Constructor name: "${constructorName}"`);
|
||||||
console.log(`Modal: Object keys: [${objectKeys.join(', ')}]`);
|
// console.log(`Modal: Object keys: [${objectKeys.join(', ')}]`);
|
||||||
|
|
||||||
// COMPREHENSIVE TEST LOGIC - Accept anything with required methods that's not our specific library classes
|
// COMPREHENSIVE TEST LOGIC - Accept anything with required methods that's not our specific library classes
|
||||||
const isRealExtension = (
|
const isRealExtension = (
|
||||||
typeof obj.getPublicKey === 'function' &&
|
typeof obj.getPublicKey === 'function' &&
|
||||||
@@ -534,14 +534,14 @@ class Modal {
|
|||||||
constructorName !== 'WindowNostr' && // Our library class
|
constructorName !== 'WindowNostr' && // Our library class
|
||||||
constructorName !== 'NostrLite' // Our main class
|
constructorName !== 'NostrLite' // Our main class
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Modal: Using comprehensive test logic:`);
|
// console.log(`Modal: Using comprehensive test logic:`);
|
||||||
console.log(` Has getPublicKey: ${typeof obj.getPublicKey === 'function'}`);
|
// console.log(` Has getPublicKey: ${typeof obj.getPublicKey === 'function'}`);
|
||||||
console.log(` Has signEvent: ${typeof obj.signEvent === 'function'}`);
|
// console.log(` Has signEvent: ${typeof obj.signEvent === 'function'}`);
|
||||||
console.log(` Not WindowNostr: ${constructorName !== 'WindowNostr'}`);
|
// console.log(` Not WindowNostr: ${constructorName !== 'WindowNostr'}`);
|
||||||
console.log(` Not NostrLite: ${constructorName !== 'NostrLite'}`);
|
// console.log(` Not NostrLite: ${constructorName !== 'NostrLite'}`);
|
||||||
console.log(` Constructor: "${constructorName}"`);
|
// console.log(` Constructor: "${constructorName}"`);
|
||||||
|
|
||||||
// Additional debugging for comparison
|
// Additional debugging for comparison
|
||||||
const extensionPropChecks = {
|
const extensionPropChecks = {
|
||||||
_isEnabled: !!obj._isEnabled,
|
_isEnabled: !!obj._isEnabled,
|
||||||
@@ -555,27 +555,27 @@ class Modal {
|
|||||||
version: !!obj.version,
|
version: !!obj.version,
|
||||||
description: !!obj.description
|
description: !!obj.description
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`Modal: Extension property analysis:`, extensionPropChecks);
|
// console.log(`Modal: Extension property analysis:`, extensionPropChecks);
|
||||||
|
|
||||||
const hasExtensionProps = !!(
|
const hasExtensionProps = !!(
|
||||||
obj._isEnabled || obj.enabled || obj.kind ||
|
obj._isEnabled || obj.enabled || obj.kind ||
|
||||||
obj._eventEmitter || obj._scope || obj._requests || obj._pubkey ||
|
obj._eventEmitter || obj._scope || obj._requests || obj._pubkey ||
|
||||||
obj.name || obj.version || obj.description
|
obj.name || obj.version || obj.description
|
||||||
);
|
);
|
||||||
|
|
||||||
const underscoreKeys = objectKeys.filter(key => key.startsWith('_'));
|
const underscoreKeys = objectKeys.filter(key => key.startsWith('_'));
|
||||||
const hexToUint8Keys = objectKeys.filter(key => key.startsWith('_hex'));
|
const hexToUint8Keys = objectKeys.filter(key => key.startsWith('_hex'));
|
||||||
console.log(`Modal: Underscore keys: [${underscoreKeys.join(', ')}]`);
|
// console.log(`Modal: Underscore keys: [${underscoreKeys.join(', ')}]`);
|
||||||
console.log(`Modal: _hex* keys: [${hexToUint8Keys.join(', ')}]`);
|
// console.log(`Modal: _hex* keys: [${hexToUint8Keys.join(', ')}]`);
|
||||||
|
|
||||||
console.log(`Modal: Additional analysis:`);
|
// console.log(`Modal: Additional analysis:`);
|
||||||
console.log(` hasExtensionProps: ${hasExtensionProps}`);
|
// console.log(` hasExtensionProps: ${hasExtensionProps}`);
|
||||||
console.log(` hasLibraryMethod (_hexToUint8Array): ${objectKeys.includes('_hexToUint8Array')}`);
|
// console.log(` hasLibraryMethod (_hexToUint8Array): ${objectKeys.includes('_hexToUint8Array')}`);
|
||||||
|
|
||||||
console.log(`Modal: COMPREHENSIVE TEST LOGIC RESULT: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`);
|
// console.log(`Modal: COMPREHENSIVE TEST LOGIC RESULT: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`);
|
||||||
console.log(`Modal: FINAL DECISION for ${constructorName}: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`);
|
// console.log(`Modal: FINAL DECISION for ${constructorName}: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`);
|
||||||
|
|
||||||
return isRealExtension;
|
return isRealExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ class Modal {
|
|||||||
// Get pubkey from extension
|
// Get pubkey from extension
|
||||||
const pubkey = await extensionObj.getPublicKey();
|
const pubkey = await extensionObj.getPublicKey();
|
||||||
console.log('Extension provided pubkey:', pubkey);
|
console.log('Extension provided pubkey:', pubkey);
|
||||||
|
|
||||||
// Set extension method with the extension object
|
// Set extension method with the extension object
|
||||||
this._setAuthMethod('extension', { pubkey, extension: extensionObj });
|
this._setAuthMethod('extension', { pubkey, extension: extensionObj });
|
||||||
|
|
||||||
@@ -700,62 +700,8 @@ class Modal {
|
|||||||
_showLocalKeyScreen() {
|
_showLocalKeyScreen() {
|
||||||
this.modalBody.innerHTML = '';
|
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');
|
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;';
|
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||||||
|
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
@@ -777,10 +723,40 @@ class Modal {
|
|||||||
const formatHint = document.createElement('div');
|
const formatHint = document.createElement('div');
|
||||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
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 = () => {
|
textarea.oninput = () => {
|
||||||
const value = textarea.value.trim();
|
const value = textarea.value.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
formatHint.textContent = '';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,33 +764,94 @@ class Modal {
|
|||||||
if (format === 'nsec') {
|
if (format === 'nsec') {
|
||||||
formatHint.textContent = '✅ Valid nsec format detected';
|
formatHint.textContent = '✅ Valid nsec format detected';
|
||||||
formatHint.style.color = '#059669';
|
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') {
|
} else if (format === 'hex') {
|
||||||
formatHint.textContent = '✅ Valid hex format detected';
|
formatHint.textContent = '✅ Valid hex format detected';
|
||||||
formatHint.style.color = '#059669';
|
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 {
|
} else {
|
||||||
formatHint.textContent = '❌ Invalid key format - must be nsec1... or 64-character hex';
|
formatHint.textContent = '❌ Invalid key format - must be nsec1... or 64-character hex';
|
||||||
formatHint.style.color = '#dc2626';
|
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');
|
const backButton = document.createElement('button');
|
||||||
backButton.textContent = 'Back';
|
backButton.textContent = 'Back';
|
||||||
backButton.onclick = () => this._showLocalKeyScreen();
|
backButton.onclick = () => this._renderLoginOptions();
|
||||||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||||
|
|
||||||
this.modalBody.appendChild(title);
|
|
||||||
this.modalBody.appendChild(description);
|
this.modalBody.appendChild(description);
|
||||||
this.modalBody.appendChild(textarea);
|
this.modalBody.appendChild(textarea);
|
||||||
this.modalBody.appendChild(formatHint);
|
this.modalBody.appendChild(formatHint);
|
||||||
this.modalBody.appendChild(importButton);
|
this.modalBody.appendChild(importButton);
|
||||||
this.modalBody.appendChild(backButton);
|
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) {
|
_detectKeyFormat(keyValue) {
|
||||||
const trimmed = keyValue.trim();
|
const trimmed = keyValue.trim();
|
||||||
|
|
||||||
@@ -956,24 +993,28 @@ class Modal {
|
|||||||
nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||||
|
|
||||||
const nsecContainer = document.createElement('div');
|
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');
|
const nsecCode = document.createElement('code');
|
||||||
nsecCode.textContent = nsec;
|
nsecCode.textContent = nsec;
|
||||||
nsecCode.style.cssText = `
|
nsecCode.style.cssText = `
|
||||||
flex: 1;
|
display: block;
|
||||||
word-break: break-all;
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
font-family: 'Courier New', monospace;
|
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(nsecCode);
|
||||||
nsecContainer.appendChild(createCopyButton(nsec, 'nsec'));
|
nsecContainer.appendChild(nsecCopyBtn);
|
||||||
nsecSection.appendChild(nsecLabel);
|
nsecSection.appendChild(nsecLabel);
|
||||||
nsecSection.appendChild(nsecContainer);
|
nsecSection.appendChild(nsecContainer);
|
||||||
|
|
||||||
@@ -984,24 +1025,28 @@ class Modal {
|
|||||||
secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||||
|
|
||||||
const secretHexContainer = document.createElement('div');
|
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');
|
const secretHexCode = document.createElement('code');
|
||||||
secretHexCode.textContent = secretKeyHex;
|
secretHexCode.textContent = secretKeyHex;
|
||||||
secretHexCode.style.cssText = `
|
secretHexCode.style.cssText = `
|
||||||
flex: 1;
|
display: block;
|
||||||
word-break: break-all;
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
font-family: 'Courier New', monospace;
|
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(secretHexCode);
|
||||||
secretHexContainer.appendChild(createCopyButton(secretKeyHex, 'hex'));
|
secretHexContainer.appendChild(secretHexCopyBtn);
|
||||||
nsecSection.appendChild(secretHexLabel);
|
nsecSection.appendChild(secretHexLabel);
|
||||||
nsecSection.appendChild(secretHexContainer);
|
nsecSection.appendChild(secretHexContainer);
|
||||||
}
|
}
|
||||||
@@ -1017,24 +1062,28 @@ class Modal {
|
|||||||
npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||||
|
|
||||||
const npubContainer = document.createElement('div');
|
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');
|
const npubCode = document.createElement('code');
|
||||||
npubCode.textContent = npub;
|
npubCode.textContent = npub;
|
||||||
npubCode.style.cssText = `
|
npubCode.style.cssText = `
|
||||||
flex: 1;
|
display: block;
|
||||||
word-break: break-all;
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
font-family: 'Courier New', monospace;
|
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(npubCode);
|
||||||
npubContainer.appendChild(createCopyButton(npub, 'npub'));
|
npubContainer.appendChild(npubCopyBtn);
|
||||||
npubSection.appendChild(npubLabel);
|
npubSection.appendChild(npubLabel);
|
||||||
npubSection.appendChild(npubContainer);
|
npubSection.appendChild(npubContainer);
|
||||||
|
|
||||||
@@ -1044,24 +1093,28 @@ class Modal {
|
|||||||
pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||||||
|
|
||||||
const pubHexContainer = document.createElement('div');
|
const pubHexContainer = document.createElement('div');
|
||||||
pubHexContainer.style.cssText = 'display: flex; align-items: flex-start;';
|
pubHexContainer.style.cssText = '';
|
||||||
|
|
||||||
const pubHexCode = document.createElement('code');
|
const pubHexCode = document.createElement('code');
|
||||||
pubHexCode.textContent = pubkeyHex;
|
pubHexCode.textContent = pubkeyHex;
|
||||||
pubHexCode.style.cssText = `
|
pubHexCode.style.cssText = `
|
||||||
flex: 1;
|
display: block;
|
||||||
word-break: break-all;
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
font-family: 'Courier New', monospace;
|
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(pubHexCode);
|
||||||
pubHexContainer.appendChild(createCopyButton(pubkeyHex, 'hex'));
|
pubHexContainer.appendChild(pubHexCopyBtn);
|
||||||
npubSection.appendChild(pubHexLabel);
|
npubSection.appendChild(pubHexLabel);
|
||||||
npubSection.appendChild(pubHexContainer);
|
npubSection.appendChild(pubHexContainer);
|
||||||
|
|
||||||
@@ -1078,62 +1131,79 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setAuthMethod(method, options = {}) {
|
_setAuthMethod(method, options = {}) {
|
||||||
// SINGLE-EXTENSION ARCHITECTURE: Handle method switching
|
// console.log('Modal: _setAuthMethod called with:', method, options);
|
||||||
console.log('Modal: _setAuthMethod called with:', method, options);
|
|
||||||
|
|
||||||
// CRITICAL: Never install facade for extension methods - leave window.nostr as the extension
|
// CRITICAL: Never install facade for extension methods - leave window.nostr as the extension
|
||||||
if (method === 'extension') {
|
if (method === 'extension') {
|
||||||
console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as 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
|
// Emit auth method selection directly for extension
|
||||||
const event = new CustomEvent('nlMethodSelected', {
|
const event = new CustomEvent('nlMethodSelected', {
|
||||||
detail: { method, ...options }
|
detail: { method, ...options }
|
||||||
});
|
});
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FOR NON-EXTENSION METHODS: Force-install facade with resilience
|
||||||
|
// console.log('Modal: Non-extension method - FORCE-INSTALLING facade with resilience:', method);
|
||||||
|
|
||||||
|
// Store the current extension if any (for potential restoration later)
|
||||||
|
const currentExtension = (window.nostr?.constructor?.name !== 'WindowNostr') ? window.nostr : null;
|
||||||
|
|
||||||
|
// Get NostrLite instance for facade operations
|
||||||
|
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
||||||
|
if (!nostrLiteInstance || typeof nostrLiteInstance._installFacade !== 'function') {
|
||||||
|
console.error('Modal: Cannot access NostrLite instance or _installFacade method');
|
||||||
|
// Fallback: emit event anyway
|
||||||
|
const event = new CustomEvent('nlMethodSelected', {
|
||||||
|
detail: { method, ...options }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-extension methods, we need to ensure WindowNostr facade is available
|
// IMMEDIATE FACADE INSTALLATION
|
||||||
console.log('Modal: Non-extension method detected:', method);
|
// console.log('Modal: Installing WindowNostr facade immediately for method:', method);
|
||||||
|
const preservedExtension = nostrLiteInstance.preservedExtension || currentExtension;
|
||||||
|
nostrLiteInstance._installFacade(preservedExtension, true);
|
||||||
|
// console.log('Modal: WindowNostr facade force-installed, current window.nostr:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
// Check if we have a preserved extension but no WindowNostr facade installed
|
// DELAYED FACADE RESILIENCE - Reinstall after extension override attempts
|
||||||
const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension;
|
const forceReinstallFacade = () => {
|
||||||
const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr';
|
console.log('Modal: RESILIENCE CHECK - Current window.nostr after delay:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
console.log('Modal: Method switching check:');
|
|
||||||
console.log(' method:', method);
|
|
||||||
console.log(' hasPreservedExtension:', hasPreservedExtension);
|
|
||||||
console.log(' hasWindowNostrFacade:', hasWindowNostrFacade);
|
|
||||||
console.log(' current window.nostr constructor:', window.nostr?.constructor?.name);
|
|
||||||
|
|
||||||
// If we have a preserved extension but no facade, install facade for method switching
|
|
||||||
if (hasPreservedExtension && !hasWindowNostrFacade) {
|
|
||||||
console.log('Modal: Installing WindowNostr facade for method switching (non-extension authentication)');
|
|
||||||
|
|
||||||
// Get the NostrLite instance and install facade with preserved extension
|
// If facade was overridden by extension, reinstall it
|
||||||
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
if (window.nostr?.constructor?.name !== 'WindowNostr') {
|
||||||
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
console.log('Modal: FACADE OVERRIDDEN! Force-reinstalling WindowNostr facade for user choice:', method);
|
||||||
const preservedExtension = nostrLiteInstance.preservedExtension;
|
nostrLiteInstance._installFacade(preservedExtension, true);
|
||||||
console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name);
|
console.log('Modal: Resilient facade force-reinstall complete, window.nostr:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
nostrLiteInstance._installFacade(preservedExtension);
|
// Schedule another check in case of persistent extension override
|
||||||
console.log('Modal: WindowNostr facade installed for method switching');
|
setTimeout(() => {
|
||||||
|
if (window.nostr?.constructor?.name !== 'WindowNostr') {
|
||||||
|
console.log('Modal: PERSISTENT OVERRIDE! Final facade force-reinstall for method:', method);
|
||||||
|
nostrLiteInstance._installFacade(preservedExtension, true);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
console.error('Modal: Cannot access NostrLite instance or _installFacade method');
|
console.log('Modal: Facade persistence verified - no override detected');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// If no extension at all, ensure facade is installed for local/NIP-46/readonly methods
|
// Schedule resilience checks at multiple intervals
|
||||||
else if (!hasPreservedExtension && !hasWindowNostrFacade) {
|
// setTimeout(forceReinstallFacade, 100); // Quick check
|
||||||
console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)');
|
// setTimeout(forceReinstallFacade, 500); // Main check
|
||||||
|
// setTimeout(forceReinstallFacade, 1500); // Final check
|
||||||
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
|
||||||
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
|
||||||
nostrLiteInstance._installFacade();
|
|
||||||
console.log('Modal: WindowNostr facade installed for non-extension methods');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit auth method selection
|
// Emit auth method selection
|
||||||
const event = new CustomEvent('nlMethodSelected', {
|
const event = new CustomEvent('nlMethodSelected', {
|
||||||
@@ -1320,7 +1390,7 @@ class Modal {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Bunker key validation failed:', error.message);
|
// console.log('Bunker key validation failed:', error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1373,7 +1443,7 @@ class Modal {
|
|||||||
|
|
||||||
async _performNip46Connect(bunkerPubkey) {
|
async _performNip46Connect(bunkerPubkey) {
|
||||||
try {
|
try {
|
||||||
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
|
// console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
|
||||||
|
|
||||||
// Check if nostr-tools NIP-46 is available
|
// Check if nostr-tools NIP-46 is available
|
||||||
if (!window.NostrTools?.nip46) {
|
if (!window.NostrTools?.nip46) {
|
||||||
@@ -1381,41 +1451,41 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use nostr-tools to parse bunker input - this handles all formats correctly
|
// Use nostr-tools to parse bunker input - this handles all formats correctly
|
||||||
console.log('Parsing bunker input with nostr-tools...');
|
// console.log('Parsing bunker input with nostr-tools...');
|
||||||
const bunkerPointer = await window.NostrTools.nip46.parseBunkerInput(bunkerPubkey);
|
const bunkerPointer = await window.NostrTools.nip46.parseBunkerInput(bunkerPubkey);
|
||||||
|
|
||||||
if (!bunkerPointer) {
|
if (!bunkerPointer) {
|
||||||
throw new Error('Unable to parse bunker connection string or resolve NIP-05 identifier');
|
throw new Error('Unable to parse bunker connection string or resolve NIP-05 identifier');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Parsed bunker pointer:', bunkerPointer);
|
// console.log('Parsed bunker pointer:', bunkerPointer);
|
||||||
|
|
||||||
// Create local client keypair for this session
|
// Create local client keypair for this session
|
||||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||||
console.log('Generated local client keypair for NIP-46 session');
|
// console.log('Generated local client keypair for NIP-46 session');
|
||||||
|
|
||||||
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
||||||
console.log('Creating nip46 BunkerSigner...');
|
// console.log('Creating nip46 BunkerSigner...');
|
||||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||||
onauth: (url) => {
|
onauth: (url) => {
|
||||||
console.log('Received auth URL from bunker:', url);
|
// console.log('Received auth URL from bunker:', url);
|
||||||
// Open auth URL in popup or redirect
|
// Open auth URL in popup or redirect
|
||||||
window.open(url, '_blank', 'width=600,height=800');
|
window.open(url, '_blank', 'width=600,height=800');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('NIP-46 BunkerSigner created successfully');
|
// console.log('NIP-46 BunkerSigner created successfully');
|
||||||
|
|
||||||
// Skip ping test - NIP-46 works through relays, not direct connection
|
// Skip ping test - NIP-46 works through relays, not direct connection
|
||||||
// Try to connect directly (this may trigger auth flow)
|
// Try to connect directly (this may trigger auth flow)
|
||||||
console.log('Attempting NIP-46 connect...');
|
// console.log('Attempting NIP-46 connect...');
|
||||||
await signer.connect();
|
await signer.connect();
|
||||||
console.log('NIP-46 connect successful');
|
// console.log('NIP-46 connect successful');
|
||||||
|
|
||||||
// Get the user's public key from the bunker
|
// Get the user's public key from the bunker
|
||||||
console.log('Getting public key from bunker...');
|
// console.log('Getting public key from bunker...');
|
||||||
const userPubkey = await signer.getPublicKey();
|
const userPubkey = await signer.getPublicKey();
|
||||||
console.log('NIP-46 user public key:', userPubkey);
|
// console.log('NIP-46 user public key:', userPubkey);
|
||||||
|
|
||||||
// Store the NIP-46 authentication info
|
// Store the NIP-46 authentication info
|
||||||
const nip46Info = {
|
const nip46Info = {
|
||||||
@@ -1429,7 +1499,7 @@ class Modal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('NOSTR_LOGIN_LITE NIP-46 connection established successfully!');
|
// console.log('NOSTR_LOGIN_LITE NIP-46 connection established successfully!');
|
||||||
|
|
||||||
// Set as current auth method
|
// Set as current auth method
|
||||||
this._setAuthMethod('nip46', nip46Info);
|
this._setAuthMethod('nip46', nip46Info);
|
||||||
@@ -1602,8 +1672,8 @@ class Modal {
|
|||||||
textarea.oninput();
|
textarea.oninput();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words');
|
// console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to generate seed phrase:', error);
|
console.error('Failed to generate seed phrase:', error);
|
||||||
formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available';
|
formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available';
|
||||||
@@ -1620,7 +1690,7 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const words = mnemonic.trim().split(/\s+/);
|
const words = mnemonic.trim().split(/\s+/);
|
||||||
|
|
||||||
// Must be 12 or 24 words
|
// Must be 12 or 24 words
|
||||||
if (words.length !== 12 && words.length !== 24) {
|
if (words.length !== 12 && words.length !== 24) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1630,7 +1700,7 @@ class Modal {
|
|||||||
window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0);
|
window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Mnemonic validation failed:', error.message);
|
// console.log('Mnemonic validation failed:', error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1672,7 +1742,7 @@ class Modal {
|
|||||||
throw new Error('Failed to derive any accounts from seed phrase');
|
throw new Error('Failed to derive any accounts from seed phrase');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Successfully derived ${accounts.length} accounts from seed phrase`);
|
// console.log(`Successfully derived ${accounts.length} accounts from seed phrase`);
|
||||||
this._showAccountSelection(accounts);
|
this._showAccountSelection(accounts);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1768,8 +1838,8 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_selectAccount(account) {
|
_selectAccount(account) {
|
||||||
console.log('Selected account:', account.index, account.npub);
|
// console.log('Selected account:', account.index, account.npub);
|
||||||
|
|
||||||
// Use the same auth method as local keys, but with seedphrase identifier
|
// Use the same auth method as local keys, but with seedphrase identifier
|
||||||
this._setAuthMethod('local', {
|
this._setAuthMethod('local', {
|
||||||
secret: account.nsec,
|
secret: account.nsec,
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user