9 Commits

Author SHA1 Message Date
Your Name
8f34c2de73 Seem to have most everything working well. Got persistant state after page refresh, and implmented logout call 2025-09-19 16:09:05 -04:00
Your Name
ca75df8bb4 Fixed issue with bunker. Made the modal more beautiful. 2025-09-19 12:24:13 -04:00
Your Name
c747f1f315 . 2025-09-18 10:18:32 -04:00
Your Name
2a66b5eeec Fixed name display 2025-09-16 18:13:01 -04:00
Your Name
fa9688b17e Implement logging in via seed phrase 2025-09-16 15:51:08 -04:00
Your Name
a0e18c34d6 Add comprehensive sign.html test and update documentation
- Add examples/sign.html with comprehensive extension compatibility testing
- Update README.md with profile fetching API documentation
- Update .gitignore for better file management
- Update examples/button.html and examples/modal.html with latest features

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

8
.gitignore vendored
View File

@@ -18,10 +18,4 @@ Thumbs.db
log.txt
Trash/
# Environment files
.env
# Aider files
.aider.chat.history.md
.aider.input.history
.aider.tags.cache.v3/
nostr-login/

View File

@@ -24,6 +24,11 @@ await NOSTR_LOGIN_LITE.init({
enabled: true,
hPosition: 0.95, // 0.0-1.0 or '95%' from left
vPosition: 0.5, // 0.0-1.0 or '50%' from top
getUserInfo: true, // Fetch user profile name from relays
getUserRelay: [ // Relays for profile queries
'wss://relay.damus.io',
'wss://nos.lol'
],
appearance: {
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
theme: 'auto', // 'auto' follows main theme

View File

@@ -51,7 +51,7 @@
<script>
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,

View File

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

184
examples/sign.html Normal file
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -58,7 +58,7 @@ class Modal {
overflow: hidden;
`;
} else {
// Modal content: centered with margin
// Modal content: centered with margin, no fixed height
modalContent.style.cssText = `
position: relative;
background: var(--nl-secondary-color);
@@ -68,7 +68,6 @@ class Modal {
margin: 50px auto;
border-radius: var(--nl-border-radius, 15px);
border: var(--nl-border-width) solid var(--nl-primary-color);
max-height: 600px;
overflow: hidden;
`;
}
@@ -105,7 +104,7 @@ class Modal {
closeButton.style.cssText = `
background: var(--nl-secondary-color);
border: var(--nl-border-width) solid var(--nl-primary-color);
border-radius: var(--nl-border-radius);
border-radius: 4px;
font-size: 28px;
color: var(--nl-primary-color);
cursor: pointer;
@@ -133,8 +132,6 @@ class Modal {
this.modalBody = document.createElement('div');
this.modalBody.style.cssText = `
padding: 24px;
overflow-y: auto;
max-height: 500px;
background: transparent;
font-family: var(--nl-font-family, 'Courier New', monospace);
`;
@@ -223,6 +220,16 @@ class Modal {
});
}
// Seed Phrase option - only show if explicitly enabled
if (this.options?.methods?.seedphrase === true) {
options.push({
type: 'seedphrase',
title: 'Seed Phrase',
description: 'Import from mnemonic seed phrase',
icon: '🌱'
});
}
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
options.push({
@@ -281,20 +288,13 @@ class Modal {
};
const iconDiv = document.createElement('div');
// Replace emoji icons with text-based ones
const iconMap = {
'🔌': '[EXT]',
'🔑': '[KEY]',
'🌐': '[NET]',
'👁️': '[VIEW]',
'📱': '[SMS]'
};
iconDiv.textContent = iconMap[option.icon] || option.icon;
// Remove the icon entirely - no emojis or text-based icons
iconDiv.textContent = '';
iconDiv.style.cssText = `
font-size: 16px;
font-weight: bold;
margin-right: 16px;
width: 50px;
width: 0px;
text-align: center;
color: var(--nl-primary-color);
font-family: var(--nl-font-family, 'Courier New', monospace);
@@ -340,6 +340,9 @@ class Modal {
case 'local':
this._showLocalKeyScreen();
break;
case 'seedphrase':
this._showSeedPhraseScreen();
break;
case 'connect':
this._showConnectScreen();
break;
@@ -353,23 +356,62 @@ class Modal {
}
_handleExtension() {
// Detect all available real extensions
const availableExtensions = this._detectAllExtensions();
// SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension
let extension = null;
console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName));
if (availableExtensions.length === 0) {
console.log('Modal: No real extensions found');
this._showExtensionRequired();
} else if (availableExtensions.length === 1) {
// Single extension - use it directly without showing choice UI
console.log('Modal: Single extension detected, using it directly:', availableExtensions[0].displayName);
this._tryExtensionLogin(availableExtensions[0].extension);
} else {
// Multiple extensions - show choice UI
console.log('Modal: Multiple extensions detected, showing choice UI for', availableExtensions.length, 'extensions');
this._showExtensionChoice(availableExtensions);
// Check if NostrLite instance has a preserved extension (real extension detected at init)
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
console.log('Modal: Using preserved extension:', extension.constructor?.name);
}
// Otherwise check current window.nostr
else if (window.nostr && this._isRealExtension(window.nostr)) {
extension = window.nostr;
console.log('Modal: Using current window.nostr extension:', extension.constructor?.name);
}
if (!extension) {
console.log('Modal: No extension detected yet, waiting for deferred detection...');
// DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library
let attempts = 0;
const maxAttempts = 10; // Try for 2 seconds
const checkForExtension = () => {
attempts++;
// Check again for preserved extension (might be set by deferred detection)
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name);
this._tryExtensionLogin(extension);
return;
}
// Check current window.nostr again
if (window.nostr && this._isRealExtension(window.nostr)) {
extension = window.nostr;
console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name);
this._tryExtensionLogin(extension);
return;
}
// Keep trying or give up
if (attempts < maxAttempts) {
setTimeout(checkForExtension, 200);
} else {
console.log('Modal: No browser extension found after waiting 2 seconds');
this._showExtensionRequired();
}
};
// Start checking after a brief delay
setTimeout(checkForExtension, 200);
return;
}
// Use the single detected extension directly - no choice UI
console.log('Modal: Single extension mode - using extension directly');
this._tryExtensionLogin(extension);
}
_detectAllExtensions() {
@@ -415,7 +457,27 @@ class Modal {
// Also check window.nostr but be extra careful to avoid our library
console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name);
if (window.nostr && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
if (window.nostr) {
// Check if window.nostr is our WindowNostr facade with a preserved extension
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
const preservedExtension = window.nostr.existingNostr;
console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
extensions.push({
name: 'window.nostr.existingNostr',
displayName: 'Extension (preserved by WindowNostr)',
icon: '🔑',
extension: preservedExtension
});
seenExtensions.add(preservedExtension);
console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`);
}
}
// Check if window.nostr is directly a real extension (not our facade)
else if (this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
extensions.push({
name: 'window.nostr',
displayName: 'Extension (window.nostr)',
@@ -424,8 +486,9 @@ class Modal {
});
seenExtensions.add(window.nostr);
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
} else if (window.nostr) {
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`);
} else {
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`);
}
}
return extensions;
@@ -831,15 +894,176 @@ class Modal {
const warningDiv = document.createElement('div');
warningDiv.textContent = '⚠️ Save your secret key securely!';
warningDiv.style.cssText = 'background: #fef3c7; color: #92400e; padding: 12px; border-radius: 6px; margin-bottom: 16px; font-size: 14px;';
warningDiv.style.cssText = 'background: #fef3c7; color: #92400e; padding: 12px; border-radius: 6px; margin-bottom: 16px; font-size: 12px;';
const nsecDiv = document.createElement('div');
nsecDiv.innerHTML = `<strong>Your Secret Key:</strong><br><code style="word-break: break-all; background: #f3f4f6; padding: 8px; border-radius: 4px;">${nsec}</code>`;
nsecDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
// Helper function to create copy button
const createCopyButton = (text, label) => {
const copyBtn = document.createElement('button');
copyBtn.textContent = `Copy ${label}`;
copyBtn.style.cssText = `
margin-left: 8px;
padding: 4px 8px;
font-size: 10px;
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: 1px solid var(--nl-primary-color);
border-radius: 4px;
cursor: pointer;
font-family: var(--nl-font-family, 'Courier New', monospace);
`;
copyBtn.onclick = async (e) => {
e.preventDefault();
try {
await navigator.clipboard.writeText(text);
const originalText = copyBtn.textContent;
copyBtn.textContent = '✓ Copied!';
copyBtn.style.color = '#059669';
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.style.color = 'var(--nl-primary-color)';
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
copyBtn.textContent = '✗ Failed';
copyBtn.style.color = '#dc2626';
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.style.color = 'var(--nl-primary-color)';
}, 2000);
}
};
return copyBtn;
};
const npubDiv = document.createElement('div');
npubDiv.innerHTML = `<strong>Your Public Key:</strong><br><code style="word-break: break-all; background: #f3f4f6; padding: 8px; border-radius: 4px;">${window.NostrTools.nip19.npubEncode(pubkey)}</code>`;
npubDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
// Convert pubkey to hex for verification
const pubkeyHex = typeof pubkey === 'string' ? pubkey : Array.from(pubkey).map(b => b.toString(16).padStart(2, '0')).join('');
// Decode nsec to get secret key as hex
let secretKeyHex = '';
try {
const decoded = window.NostrTools.nip19.decode(nsec);
secretKeyHex = Array.from(decoded.data).map(b => b.toString(16).padStart(2, '0')).join('');
} catch (err) {
console.error('Failed to decode nsec for hex display:', err);
}
// Secret Key Section
const nsecSection = document.createElement('div');
nsecSection.style.cssText = 'margin-bottom: 16px;';
const nsecLabel = document.createElement('div');
nsecLabel.innerHTML = '<strong>Your Secret Key (nsec):</strong>';
nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
const nsecContainer = document.createElement('div');
nsecContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
const nsecCode = document.createElement('code');
nsecCode.textContent = nsec;
nsecCode.style.cssText = `
flex: 1;
word-break: break-all;
background: #f3f4f6;
padding: 6px;
border-radius: 4px;
font-size: 10px;
line-height: 1.3;
font-family: 'Courier New', monospace;
display: block;
`;
nsecContainer.appendChild(nsecCode);
nsecContainer.appendChild(createCopyButton(nsec, 'nsec'));
nsecSection.appendChild(nsecLabel);
nsecSection.appendChild(nsecContainer);
// Secret Key Hex Section
if (secretKeyHex) {
const secretHexLabel = document.createElement('div');
secretHexLabel.innerHTML = '<strong>Secret Key (hex):</strong>';
secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
const secretHexContainer = document.createElement('div');
secretHexContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
const secretHexCode = document.createElement('code');
secretHexCode.textContent = secretKeyHex;
secretHexCode.style.cssText = `
flex: 1;
word-break: break-all;
background: #f3f4f6;
padding: 6px;
border-radius: 4px;
font-size: 10px;
line-height: 1.3;
font-family: 'Courier New', monospace;
display: block;
`;
secretHexContainer.appendChild(secretHexCode);
secretHexContainer.appendChild(createCopyButton(secretKeyHex, 'hex'));
nsecSection.appendChild(secretHexLabel);
nsecSection.appendChild(secretHexContainer);
}
// Public Key Section
const npubSection = document.createElement('div');
npubSection.style.cssText = 'margin-bottom: 16px;';
const npub = window.NostrTools.nip19.npubEncode(pubkey);
const npubLabel = document.createElement('div');
npubLabel.innerHTML = '<strong>Your Public Key (npub):</strong>';
npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
const npubContainer = document.createElement('div');
npubContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
const npubCode = document.createElement('code');
npubCode.textContent = npub;
npubCode.style.cssText = `
flex: 1;
word-break: break-all;
background: #f3f4f6;
padding: 6px;
border-radius: 4px;
font-size: 10px;
line-height: 1.3;
font-family: 'Courier New', monospace;
display: block;
`;
npubContainer.appendChild(npubCode);
npubContainer.appendChild(createCopyButton(npub, 'npub'));
npubSection.appendChild(npubLabel);
npubSection.appendChild(npubContainer);
// Public Key Hex Section
const pubHexLabel = document.createElement('div');
pubHexLabel.innerHTML = '<strong>Public Key (hex):</strong>';
pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
const pubHexContainer = document.createElement('div');
pubHexContainer.style.cssText = 'display: flex; align-items: flex-start;';
const pubHexCode = document.createElement('code');
pubHexCode.textContent = pubkeyHex;
pubHexCode.style.cssText = `
flex: 1;
word-break: break-all;
background: #f3f4f6;
padding: 6px;
border-radius: 4px;
font-size: 10px;
line-height: 1.3;
font-family: 'Courier New', monospace;
display: block;
`;
pubHexContainer.appendChild(pubHexCode);
pubHexContainer.appendChild(createCopyButton(pubkeyHex, 'hex'));
npubSection.appendChild(pubHexLabel);
npubSection.appendChild(pubHexContainer);
const continueButton = document.createElement('button');
continueButton.textContent = 'Continue';
@@ -848,12 +1072,69 @@ class Modal {
this.modalBody.appendChild(title);
this.modalBody.appendChild(warningDiv);
this.modalBody.appendChild(nsecDiv);
this.modalBody.appendChild(npubDiv);
this.modalBody.appendChild(nsecSection);
this.modalBody.appendChild(npubSection);
this.modalBody.appendChild(continueButton);
}
_setAuthMethod(method, options = {}) {
// SINGLE-EXTENSION ARCHITECTURE: Handle method switching
console.log('Modal: _setAuthMethod called with:', method, options);
// CRITICAL: Never install facade for extension methods - leave window.nostr as the extension
if (method === 'extension') {
console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension');
// Emit auth method selection directly for extension
const event = new CustomEvent('nlMethodSelected', {
detail: { method, ...options }
});
window.dispatchEvent(event);
this.close();
return;
}
// For non-extension methods, we need to ensure WindowNostr facade is available
console.log('Modal: Non-extension method detected:', method);
// Check if we have a preserved extension but no WindowNostr facade installed
const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension;
const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr';
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
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
const preservedExtension = nostrLiteInstance.preservedExtension;
console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name);
nostrLiteInstance._installFacade(preservedExtension);
console.log('Modal: WindowNostr facade installed for method switching');
} else {
console.error('Modal: Cannot access NostrLite instance or _installFacade method');
}
}
// If no extension at all, ensure facade is installed for local/NIP-46/readonly methods
else if (!hasPreservedExtension && !hasWindowNostrFacade) {
console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)');
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
const event = new CustomEvent('nlMethodSelected', {
detail: { method, ...options }
@@ -887,8 +1168,13 @@ class Modal {
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
const message = document.createElement('p');
message.textContent = 'Please install a Nostr browser extension like Alby or getflattr and refresh the page.';
message.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
message.innerHTML = `
Please install a Nostr browser extension and refresh the page.<br><br>
<strong>Important:</strong> If you have multiple extensions installed, please disable all but one to avoid conflicts.
<br><br>
Popular extensions: Alby, nos2x, Flamingo
`;
message.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px; line-height: 1.4;';
const backButton = document.createElement('button');
backButton.textContent = 'Back';
@@ -903,10 +1189,6 @@ class Modal {
_showConnectScreen() {
this.modalBody.innerHTML = '';
const title = document.createElement('h3');
title.textContent = 'Connect to NIP-46 Remote Signer';
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
const description = document.createElement('p');
description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.';
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
@@ -931,28 +1213,67 @@ class Modal {
box-sizing: border-box;
`;
const urlLabel = document.createElement('label');
urlLabel.textContent = 'Remote URL (optional):';
urlLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
const urlInput = document.createElement('input');
urlInput.type = 'url';
urlInput.placeholder = 'ws://localhost:8080 (default)';
urlInput.style.cssText = `
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
margin-bottom: 16px;
box-sizing: border-box;
`;
// Users will enter the bunker URL manually from their bunker setup
// Add real-time bunker key validation
const formatHint = document.createElement('div');
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
const connectButton = document.createElement('button');
connectButton.textContent = 'Connect to Bunker';
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, urlInput.value);
connectButton.style.cssText = this._getButtonStyle();
connectButton.disabled = true;
connectButton.onclick = () => {
if (!connectButton.disabled) {
this._handleNip46Connect(pubkeyInput.value);
}
};
// Set initial disabled state
connectButton.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);
margin-bottom: 12px;
`;
pubkeyInput.oninput = () => {
const value = pubkeyInput.value.trim();
if (!value) {
formatHint.textContent = '';
// Disable button
connectButton.disabled = true;
connectButton.style.borderColor = 'var(--nl-muted-color)';
connectButton.style.color = 'var(--nl-muted-color)';
connectButton.style.cursor = 'not-allowed';
return;
}
const isValid = this._validateBunkerKey(value);
if (isValid) {
formatHint.textContent = '✅ Valid bunker connection format detected';
formatHint.style.color = '#059669';
// Enable button
connectButton.disabled = false;
connectButton.style.borderColor = 'var(--nl-primary-color)';
connectButton.style.color = 'var(--nl-primary-color)';
connectButton.style.cursor = 'pointer';
} else {
formatHint.textContent = '❌ Invalid format - must be bunker://, npub, or 64-char hex';
formatHint.style.color = '#dc2626';
// Disable button
connectButton.disabled = true;
connectButton.style.borderColor = 'var(--nl-muted-color)';
connectButton.style.color = 'var(--nl-muted-color)';
connectButton.style.cursor = 'not-allowed';
}
};
const backButton = document.createElement('button');
backButton.textContent = 'Back';
@@ -961,27 +1282,60 @@ class Modal {
formGroup.appendChild(label);
formGroup.appendChild(pubkeyInput);
formGroup.appendChild(urlLabel);
formGroup.appendChild(urlInput);
formGroup.appendChild(formatHint);
this.modalBody.appendChild(title);
this.modalBody.appendChild(description);
this.modalBody.appendChild(formGroup);
this.modalBody.appendChild(connectButton);
this.modalBody.appendChild(backButton);
}
_handleNip46Connect(bunkerPubkey, bunkerUrl) {
_validateBunkerKey(bunkerKey) {
try {
const trimmed = bunkerKey.trim();
// Check for bunker:// format
if (trimmed.startsWith('bunker://')) {
// Should have format: bunker://pubkey or bunker://pubkey?param=value
const match = trimmed.match(/^bunker:\/\/([0-9a-fA-F]{64})(\?.*)?$/);
return !!match;
}
// Check for npub format
if (trimmed.startsWith('npub1') && trimmed.length === 63) {
try {
if (window.NostrTools?.nip19) {
const decoded = window.NostrTools.nip19.decode(trimmed);
return decoded.type === 'npub';
}
} catch {
return false;
}
}
// Check for hex format (64 characters, valid hex)
if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) {
return true;
}
return false;
} catch (error) {
console.log('Bunker key validation failed:', error.message);
return false;
}
}
_handleNip46Connect(bunkerPubkey) {
if (!bunkerPubkey || !bunkerPubkey.length) {
this._showError('Bunker pubkey is required');
return;
}
this._showNip46Connecting(bunkerPubkey, bunkerUrl);
this._performNip46Connect(bunkerPubkey, bunkerUrl);
this._showNip46Connecting(bunkerPubkey);
this._performNip46Connect(bunkerPubkey);
}
_showNip46Connecting(bunkerPubkey, bunkerUrl) {
_showNip46Connecting(bunkerPubkey) {
this.modalBody.innerHTML = '';
const title = document.createElement('h3');
@@ -999,9 +1353,8 @@ class Modal {
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
bunkerInfo.innerHTML = `
<strong>Connecting to bunker:</strong><br>
Pubkey: <code style="word-break: break-all;">${displayPubkey}</code><br>
Relay: <code style="word-break: break-all;">${bunkerUrl || 'ws://localhost:8080'}</code><br>
<small style="color: #6b7280;">If this relay is offline, the bunker server may be unavailable.</small>
Connection: <code style="word-break: break-all;">${displayPubkey}</code><br>
<small style="color: #6b7280;">Connection string contains all necessary relay information.</small>
`;
const connectingDiv = document.createElement('div');
@@ -1018,9 +1371,9 @@ class Modal {
this.modalBody.appendChild(connectingDiv);
}
async _performNip46Connect(bunkerPubkey, bunkerUrl) {
async _performNip46Connect(bunkerPubkey) {
try {
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey, bunkerUrl);
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
// Check if nostr-tools NIP-46 is available
if (!window.NostrTools?.nip46) {
@@ -1041,9 +1394,9 @@ class Modal {
const localSecretKey = window.NostrTools.generateSecretKey();
console.log('Generated local client keypair for NIP-46 session');
// Use nostr-tools BunkerSigner constructor
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
console.log('Creating nip46 BunkerSigner...');
const signer = new window.NostrTools.nip46.BunkerSigner(localSecretKey, bunkerPointer, {
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
onauth: (url) => {
console.log('Received auth URL from bunker:', url);
// Open auth URL in popup or redirect
@@ -1120,6 +1473,312 @@ class Modal {
this._setAuthMethod('readonly');
}
_showSeedPhraseScreen() {
this.modalBody.innerHTML = '';
const description = document.createElement('p');
description.innerHTML = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts, or <span id="generate-new" style="text-decoration: underline; cursor: pointer; color: var(--nl-primary-color);">generate new</span>.';
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
const textarea = document.createElement('textarea');
// Remove default placeholder text as requested
textarea.placeholder = '';
textarea.style.cssText = `
width: 100%;
height: 100px;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
margin-bottom: 12px;
resize: none;
font-family: monospace;
font-size: 14px;
box-sizing: border-box;
`;
// Add real-time mnemonic validation
const formatHint = document.createElement('div');
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
const importButton = document.createElement('button');
importButton.textContent = 'Import Accounts';
importButton.disabled = true;
importButton.onclick = () => {
if (!importButton.disabled) {
this._importFromSeedPhrase(textarea.value);
}
};
// Set initial disabled state
importButton.style.cssText = `
display: block;
width: 100%;
padding: 12px;
border: var(--nl-border-width) solid var(--nl-muted-color);
border-radius: var(--nl-border-radius);
font-size: 16px;
font-weight: 500;
cursor: not-allowed;
transition: all 0.2s;
font-family: var(--nl-font-family, 'Courier New', monospace);
background: var(--nl-secondary-color);
color: var(--nl-muted-color);
`;
textarea.oninput = () => {
const value = textarea.value.trim();
if (!value) {
formatHint.textContent = '';
// Disable button
importButton.disabled = true;
importButton.style.borderColor = 'var(--nl-muted-color)';
importButton.style.color = 'var(--nl-muted-color)';
importButton.style.cursor = 'not-allowed';
return;
}
const isValid = this._validateMnemonic(value);
if (isValid) {
const wordCount = value.split(/\s+/).length;
formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`;
formatHint.style.color = '#059669';
// Enable button
importButton.disabled = false;
importButton.style.borderColor = 'var(--nl-primary-color)';
importButton.style.color = 'var(--nl-primary-color)';
importButton.style.cursor = 'pointer';
} else {
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
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 backButton = document.createElement('button');
backButton.textContent = 'Back';
backButton.onclick = () => this._renderLoginOptions();
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
this.modalBody.appendChild(description);
this.modalBody.appendChild(textarea);
this.modalBody.appendChild(formatHint);
this.modalBody.appendChild(importButton);
this.modalBody.appendChild(backButton);
// Add click handler for the "generate new" link
const generateLink = document.getElementById('generate-new');
if (generateLink) {
generateLink.addEventListener('mouseenter', () => {
generateLink.style.color = 'var(--nl-accent-color)';
});
generateLink.addEventListener('mouseleave', () => {
generateLink.style.color = 'var(--nl-primary-color)';
});
generateLink.addEventListener('click', () => {
this._generateNewSeedPhrase(textarea, formatHint);
});
}
}
_generateNewSeedPhrase(textarea, formatHint) {
try {
// Check if NIP-06 is available
if (!window.NostrTools?.nip06) {
throw new Error('NIP-06 not available in bundle');
}
// Generate a random 12-word mnemonic using NostrTools
const mnemonic = window.NostrTools.nip06.generateSeedWords();
// Set the generated mnemonic in the textarea
textarea.value = mnemonic;
// Trigger the oninput event to properly validate and enable the button
if (textarea.oninput) {
textarea.oninput();
}
console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words');
} catch (error) {
console.error('Failed to generate seed phrase:', error);
formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available';
formatHint.style.color = '#dc2626';
}
}
_validateMnemonic(mnemonic) {
try {
// Check if NIP-06 is available
if (!window.NostrTools?.nip06) {
console.error('NIP-06 not available in bundle');
return false;
}
const words = mnemonic.trim().split(/\s+/);
// Must be 12 or 24 words
if (words.length !== 12 && words.length !== 24) {
return false;
}
// Try to validate using NostrTools nip06 - this will throw if invalid
window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0);
return true;
} catch (error) {
console.log('Mnemonic validation failed:', error.message);
return false;
}
}
_importFromSeedPhrase(mnemonic) {
try {
const trimmed = mnemonic.trim();
if (!trimmed) {
throw new Error('Please enter a mnemonic seed phrase');
}
// Validate the mnemonic
if (!this._validateMnemonic(trimmed)) {
throw new Error('Invalid mnemonic. Please enter a valid 12 or 24-word BIP-39 seed phrase');
}
// Generate accounts 0-5 using NIP-06
const accounts = [];
for (let i = 0; i < 6; i++) {
try {
const privateKey = window.NostrTools.nip06.privateKeyFromSeedWords(trimmed, '', i);
const publicKey = window.NostrTools.getPublicKey(privateKey);
const nsec = window.NostrTools.nip19.nsecEncode(privateKey);
const npub = window.NostrTools.nip19.npubEncode(publicKey);
accounts.push({
index: i,
privateKey,
publicKey,
nsec,
npub
});
} catch (error) {
console.error(`Failed to derive account ${i}:`, error);
}
}
if (accounts.length === 0) {
throw new Error('Failed to derive any accounts from seed phrase');
}
console.log(`Successfully derived ${accounts.length} accounts from seed phrase`);
this._showAccountSelection(accounts);
} catch (error) {
console.error('Seed phrase import failed:', error);
this._showError('Seed phrase import failed: ' + error.message);
}
}
_showAccountSelection(accounts) {
this.modalBody.innerHTML = '';
const description = document.createElement('p');
description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`;
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
this.modalBody.appendChild(description);
// Create table for account selection
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
font-family: var(--nl-font-family, 'Courier New', monospace);
font-size: 12px;
`;
// Table header
const thead = document.createElement('thead');
thead.innerHTML = `
<tr style="background: #f3f4f6;">
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">#</th>
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Use</th>
</tr>
`;
table.appendChild(thead);
// Table body
const tbody = document.createElement('tbody');
accounts.forEach(account => {
const row = document.createElement('tr');
row.style.cssText = 'border: 1px solid #d1d5db;';
const indexCell = document.createElement('td');
indexCell.textContent = account.index;
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
const actionCell = document.createElement('td');
actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;';
// Show truncated npub in the button
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
const selectButton = document.createElement('button');
selectButton.textContent = truncatedNpub;
selectButton.onclick = () => this._selectAccount(account);
selectButton.style.cssText = `
width: 100%;
padding: 8px 12px;
font-size: 11px;
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: 1px solid var(--nl-primary-color);
border-radius: 4px;
cursor: pointer;
font-family: 'Courier New', monospace;
text-align: center;
`;
selectButton.onmouseover = () => {
selectButton.style.borderColor = 'var(--nl-accent-color)';
};
selectButton.onmouseout = () => {
selectButton.style.borderColor = 'var(--nl-primary-color)';
};
actionCell.appendChild(selectButton);
row.appendChild(indexCell);
row.appendChild(actionCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
this.modalBody.appendChild(table);
// Back button
const backButton = document.createElement('button');
backButton.textContent = 'Back to Seed Phrase';
backButton.onclick = () => this._showSeedPhraseScreen();
backButton.style.cssText = this._getButtonStyle('secondary');
this.modalBody.appendChild(backButton);
}
_selectAccount(account) {
console.log('Selected account:', account.index, account.npub);
// Use the same auth method as local keys, but with seedphrase identifier
this._setAuthMethod('local', {
secret: account.nsec,
pubkey: account.publicKey,
source: 'seedphrase',
accountIndex: account.index
});
}
_showOtpScreen() {
// Placeholder for OTP functionality
this._showError('OTP/DM not yet implemented - coming soon!');

1
nostr-tools Submodule

Submodule nostr-tools added at 23aebbd341