Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e18c34d6 | ||
|
|
995c3f526c | ||
|
|
77ea4a8e67 | ||
|
|
12d4810f4c |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -18,10 +18,4 @@ Thumbs.db
|
|||||||
log.txt
|
log.txt
|
||||||
Trash/
|
Trash/
|
||||||
|
|
||||||
# Environment files
|
nostr-login/
|
||||||
.env
|
|
||||||
|
|
||||||
# Aider files
|
|
||||||
.aider.chat.history.md
|
|
||||||
.aider.input.history
|
|
||||||
.aider.tags.cache.v3/
|
|
||||||
@@ -24,6 +24,11 @@ await NOSTR_LOGIN_LITE.init({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
hPosition: 0.95, // 0.0-1.0 or '95%' from left
|
hPosition: 0.95, // 0.0-1.0 or '95%' from left
|
||||||
vPosition: 0.5, // 0.0-1.0 or '50%' from top
|
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: {
|
appearance: {
|
||||||
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
||||||
theme: 'auto', // 'auto' follows main theme
|
theme: 'auto', // 'auto' follows main theme
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await window.NOSTR_LOGIN_LITE.init({
|
await window.NOSTR_LOGIN_LITE.init({
|
||||||
theme: 'default',
|
|
||||||
methods: {
|
methods: {
|
||||||
extension: true,
|
extension: true,
|
||||||
local: true,
|
local: true,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await window.NOSTR_LOGIN_LITE.init({
|
await window.NOSTR_LOGIN_LITE.init({
|
||||||
theme: 'dark',
|
theme: 'default',
|
||||||
methods: {
|
methods: {
|
||||||
extension: true,
|
extension: true,
|
||||||
local: true,
|
local: true,
|
||||||
@@ -48,12 +48,11 @@
|
|||||||
},
|
},
|
||||||
floatingTab: {
|
floatingTab: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
hPosition: 0.7, // 0.0-1.0 or '95%' from left
|
hPosition: 1, // 0.0-1.0 or '95%' from left
|
||||||
vPosition: 0.5, // 0.0-1.0 or '50%' from top
|
vPosition: 0, // 0.0-1.0 or '50%' from top
|
||||||
appearance: {
|
appearance: {
|
||||||
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
style: 'square', // 'pill', 'square', 'circle', 'minimal'
|
||||||
theme: 'auto', // 'auto' follows main theme
|
// icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
|
||||||
icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
|
|
||||||
text: 'Login'
|
text: 'Login'
|
||||||
},
|
},
|
||||||
behavior: {
|
behavior: {
|
||||||
|
|||||||
184
examples/sign.html
Normal file
184
examples/sign.html
Normal 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>
|
||||||
267
lite/build.js
267
lite/build.js
@@ -675,7 +675,7 @@ class FloatingTab {
|
|||||||
appearance: {
|
appearance: {
|
||||||
style: 'pill', // 'pill', 'square', 'circle'
|
style: 'pill', // 'pill', 'square', 'circle'
|
||||||
theme: 'auto', // 'auto', 'light', 'dark'
|
theme: 'auto', // 'auto', 'light', 'dark'
|
||||||
icon: '[LOGIN]',
|
icon: '',
|
||||||
text: 'Login',
|
text: 'Login',
|
||||||
iconOnly: false
|
iconOnly: false
|
||||||
},
|
},
|
||||||
@@ -685,11 +685,14 @@ class FloatingTab {
|
|||||||
autoSlide: true,
|
autoSlide: true,
|
||||||
persistent: false
|
persistent: false
|
||||||
},
|
},
|
||||||
|
getUserInfo: false,
|
||||||
|
getUserRelay: [],
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isAuthenticated = false;
|
this.isAuthenticated = false;
|
||||||
this.userInfo = null;
|
this.userInfo = null;
|
||||||
|
this.userProfile = null;
|
||||||
this.container = null;
|
this.container = null;
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
|
|
||||||
@@ -790,11 +793,24 @@ class FloatingTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleAuth(authData) {
|
async _handleAuth(authData) {
|
||||||
console.log('FloatingTab: Handling authentication:', authData);
|
console.log('FloatingTab: Handling authentication:', authData);
|
||||||
this.isAuthenticated = true;
|
this.isAuthenticated = true;
|
||||||
this.userInfo = authData;
|
this.userInfo = authData;
|
||||||
|
|
||||||
|
// Fetch user profile if enabled and we have a pubkey
|
||||||
|
if (this.options.getUserInfo && authData.pubkey) {
|
||||||
|
console.log('FloatingTab: Fetching user profile for:', authData.pubkey);
|
||||||
|
try {
|
||||||
|
const profile = await this._fetchUserProfile(authData.pubkey);
|
||||||
|
this.userProfile = profile;
|
||||||
|
console.log('FloatingTab: User profile fetched:', profile);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('FloatingTab: Failed to fetch user profile:', error);
|
||||||
|
this.userProfile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options.behavior.hideWhenAuthenticated) {
|
if (this.options.behavior.hideWhenAuthenticated) {
|
||||||
this.hide();
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
@@ -806,6 +822,7 @@ class FloatingTab {
|
|||||||
console.log('FloatingTab: Handling logout');
|
console.log('FloatingTab: Handling logout');
|
||||||
this.isAuthenticated = false;
|
this.isAuthenticated = false;
|
||||||
this.userInfo = null;
|
this.userInfo = null;
|
||||||
|
this.userProfile = null;
|
||||||
|
|
||||||
if (this.options.behavior.hideWhenAuthenticated) {
|
if (this.options.behavior.hideWhenAuthenticated) {
|
||||||
this.show();
|
this.show();
|
||||||
@@ -873,18 +890,29 @@ class FloatingTab {
|
|||||||
|
|
||||||
// Update content
|
// Update content
|
||||||
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
|
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
|
||||||
const display = this.userInfo?.pubkey ?
|
let display;
|
||||||
(this.options.appearance.iconOnly ?
|
|
||||||
'[USER]' :
|
// Use profile name if available, otherwise fall back to pubkey
|
||||||
\`[USER] \${this.userInfo.pubkey.slice(0, 6)}...\`) :
|
if (this.userProfile?.name || this.userProfile?.display_name) {
|
||||||
(this.options.appearance.iconOnly ? '[AUTH]' : '[AUTH] Logged In');
|
const userName = this.userProfile.name || this.userProfile.display_name;
|
||||||
|
display = this.options.appearance.iconOnly
|
||||||
|
? userName.slice(0, 8)
|
||||||
|
: userName;
|
||||||
|
} else if (this.userInfo?.pubkey) {
|
||||||
|
// Fallback to pubkey display
|
||||||
|
display = this.options.appearance.iconOnly
|
||||||
|
? this.userInfo.pubkey.slice(0, 6)
|
||||||
|
: \`\${this.userInfo.pubkey.slice(0, 6)}...\`;
|
||||||
|
} else {
|
||||||
|
display = this.options.appearance.iconOnly ? 'User' : 'Authenticated';
|
||||||
|
}
|
||||||
|
|
||||||
this.container.textContent = display;
|
this.container.textContent = display;
|
||||||
this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
|
this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
|
||||||
} else {
|
} else {
|
||||||
const display = this.options.appearance.iconOnly ?
|
const display = this.options.appearance.iconOnly ?
|
||||||
this.options.appearance.icon :
|
this.options.appearance.icon :
|
||||||
\`\${this.options.appearance.icon} \${this.options.appearance.text}\`;
|
(this.options.appearance.icon ? \`\${this.options.appearance.icon} \${this.options.appearance.text}\` : this.options.appearance.text);
|
||||||
|
|
||||||
this.container.textContent = display;
|
this.container.textContent = display;
|
||||||
this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
|
this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
|
||||||
@@ -915,6 +943,62 @@ class FloatingTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _fetchUserProfile(pubkey) {
|
||||||
|
if (!this.options.getUserInfo) {
|
||||||
|
console.log('FloatingTab: getUserInfo disabled, skipping profile fetch');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which relays to use
|
||||||
|
const relays = this.options.getUserRelay.length > 0
|
||||||
|
? this.options.getUserRelay
|
||||||
|
: (this.modal?.options?.relays || ['wss://relay.damus.io', 'wss://nos.lol']);
|
||||||
|
|
||||||
|
console.log('FloatingTab: Fetching profile from relays:', relays);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a SimplePool instance for querying
|
||||||
|
const pool = new window.NostrTools.nip46.SimplePool();
|
||||||
|
|
||||||
|
// Query for kind 0 (user metadata) events
|
||||||
|
const events = await pool.querySync(relays, {
|
||||||
|
kinds: [0],
|
||||||
|
authors: [pubkey],
|
||||||
|
limit: 1
|
||||||
|
}, { timeout: 5000 });
|
||||||
|
|
||||||
|
console.log('FloatingTab: Profile query returned', events.length, 'events');
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
console.log('FloatingTab: No profile events found');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recent event
|
||||||
|
const latestEvent = events.sort((a, b) => b.created_at - a.created_at)[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(latestEvent.content);
|
||||||
|
console.log('FloatingTab: Parsed profile:', profile);
|
||||||
|
|
||||||
|
// Return relevant profile fields
|
||||||
|
return {
|
||||||
|
name: profile.name || null,
|
||||||
|
display_name: profile.display_name || null,
|
||||||
|
about: profile.about || null,
|
||||||
|
picture: profile.picture || null,
|
||||||
|
nip05: profile.nip05 || null
|
||||||
|
};
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('FloatingTab: Failed to parse profile JSON:', parseError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FloatingTab: Profile fetch error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_position() {
|
_position() {
|
||||||
if (!this.container) return;
|
if (!this.container) return;
|
||||||
|
|
||||||
@@ -1076,7 +1160,7 @@ class NostrLite {
|
|||||||
appearance: {
|
appearance: {
|
||||||
style: 'pill',
|
style: 'pill',
|
||||||
theme: 'auto',
|
theme: 'auto',
|
||||||
icon: '[LOGIN]',
|
icon: '',
|
||||||
text: 'Login',
|
text: 'Login',
|
||||||
iconOnly: false
|
iconOnly: false
|
||||||
},
|
},
|
||||||
@@ -1085,7 +1169,9 @@ class NostrLite {
|
|||||||
showUserInfo: true,
|
showUserInfo: true,
|
||||||
autoSlide: true,
|
autoSlide: true,
|
||||||
persistent: false
|
persistent: false
|
||||||
}
|
},
|
||||||
|
getUserInfo: false,
|
||||||
|
getUserRelay: []
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
@@ -1114,16 +1200,149 @@ class NostrLite {
|
|||||||
|
|
||||||
_setupWindowNostrFacade() {
|
_setupWindowNostrFacade() {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === TRUE SINGLE-EXTENSION ARCHITECTURE ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Initial window.nostr:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Initial window.nostr constructor:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
// Store existing window.nostr if it exists (from extensions)
|
// Store existing window.nostr if it exists (from extensions)
|
||||||
const existingNostr = window.nostr;
|
const existingNostr = window.nostr;
|
||||||
|
|
||||||
// Always install our facade
|
// TRUE SINGLE-EXTENSION ARCHITECTURE: Don't install facade when extensions detected
|
||||||
window.nostr = new WindowNostr(this, existingNostr);
|
if (this._isRealExtension(existingNostr)) {
|
||||||
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed',
|
console.log('NOSTR_LOGIN_LITE: ✓ REAL EXTENSION DETECTED IMMEDIATELY - PRESERVING WITHOUT FACADE');
|
||||||
existingNostr ? '(with extension passthrough)' : '(no existing extension)');
|
console.log('NOSTR_LOGIN_LITE: Extension constructor:', existingNostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(existingNostr));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility');
|
||||||
|
this.preservedExtension = existingNostr;
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
// DON'T install facade - leave window.nostr as the extension
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEFERRED EXTENSION DETECTION: Extensions like nos2x may load after us
|
||||||
|
console.log('NOSTR_LOGIN_LITE: No real extension detected initially, starting deferred detection...');
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
|
||||||
|
let checkCount = 0;
|
||||||
|
const maxChecks = 10; // Check for up to 2 seconds
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
checkCount++;
|
||||||
|
const currentNostr = window.nostr;
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === DEFERRED CHECK ' + checkCount + '/' + maxChecks + ' ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Current window.nostr:', currentNostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Constructor:', currentNostr?.constructor?.name);
|
||||||
|
|
||||||
|
// Skip if it's our facade
|
||||||
|
if (currentNostr?.constructor?.name === 'WindowNostr') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Skipping - this is our facade');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isRealExtension(currentNostr)) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✓✓✓ LATE EXTENSION DETECTED - PRESERVING WITHOUT FACADE ✓✓✓');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension detected after ' + (checkCount * 200) + 'ms!');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension constructor:', currentNostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(currentNostr));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility');
|
||||||
|
this.preservedExtension = currentNostr;
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
// DON'T install facade - leave window.nostr as the extension
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop checking after max attempts - no extension found
|
||||||
|
if (checkCount >= maxChecks) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ⚠️ MAX CHECKS REACHED - NO EXTENSION FOUND');
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Installing facade for local/NIP-46/readonly methods');
|
||||||
|
this._installFacade();
|
||||||
|
}
|
||||||
|
}, 200); // Check every 200ms
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Waiting for deferred detection to complete...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_installFacade(existingNostr = null) {
|
||||||
|
if (typeof window !== 'undefined' && !this.facadeInstalled) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === _installFacade CALLED ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
|
const facade = new WindowNostr(this, existingNostr);
|
||||||
|
window.nostr = facade;
|
||||||
|
this.facadeInstalled = true;
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === FACADE INSTALLED WITH EXTENSION ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to identify real browser extensions
|
||||||
|
_isRealExtension(obj) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === _isRealExtension DEBUG ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: obj:', obj);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj);
|
||||||
|
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Not an object');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Object keys:', Object.keys(obj));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: getPublicKey type:', typeof obj.getPublicKey);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: signEvent type:', typeof obj.signEvent);
|
||||||
|
|
||||||
|
// Must have required Nostr methods
|
||||||
|
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Missing required methods');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude our own library classes
|
||||||
|
const constructorName = obj.constructor?.name;
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName);
|
||||||
|
|
||||||
|
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Is our library class');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude NostrTools library object
|
||||||
|
if (obj === window.NostrTools) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real extensions typically have internal properties or specific characteristics
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension property check:');
|
||||||
|
console.log(' _isEnabled:', !!obj._isEnabled);
|
||||||
|
console.log(' enabled:', !!obj.enabled);
|
||||||
|
console.log(' kind:', !!obj.kind);
|
||||||
|
console.log(' _eventEmitter:', !!obj._eventEmitter);
|
||||||
|
console.log(' _scope:', !!obj._scope);
|
||||||
|
console.log(' _requests:', !!obj._requests);
|
||||||
|
console.log(' _pubkey:', !!obj._pubkey);
|
||||||
|
console.log(' name:', !!obj.name);
|
||||||
|
console.log(' version:', !!obj.version);
|
||||||
|
console.log(' description:', !!obj.description);
|
||||||
|
|
||||||
|
const hasExtensionProps = !!(
|
||||||
|
obj._isEnabled || obj.enabled || obj.kind ||
|
||||||
|
obj._eventEmitter || obj._scope || obj._requests || obj._pubkey ||
|
||||||
|
obj.name || obj.version || obj.description
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension detection result for', constructorName, ':', hasExtensionProps);
|
||||||
|
return hasExtensionProps;
|
||||||
|
}
|
||||||
|
|
||||||
launch(startScreen = 'login') {
|
launch(startScreen = 'login') {
|
||||||
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
||||||
|
|
||||||
@@ -1254,13 +1473,13 @@ class WindowNostr {
|
|||||||
if (event.detail.method === 'extension') {
|
if (event.detail.method === 'extension') {
|
||||||
this.authenticatedExtension = event.detail.extension;
|
this.authenticatedExtension = event.detail.extension;
|
||||||
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
|
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
|
||||||
|
}
|
||||||
// Re-install our facade to ensure we intercept signEvent calls
|
|
||||||
// Extensions may overwrite window.nostr after authentication
|
// CRITICAL FIX: Re-install our facade for ALL authentication methods
|
||||||
if (typeof window !== 'undefined') {
|
// Extensions may overwrite window.nostr after ANY authentication, not just extension auth
|
||||||
console.log('WindowNostr: Re-installing facade after authentication');
|
if (typeof window !== 'undefined') {
|
||||||
window.nostr = this;
|
console.log('WindowNostr: Re-installing facade after', this.authState?.method, 'authentication');
|
||||||
}
|
window.nostr = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('WindowNostr: Auth state updated:', this.authState?.method);
|
console.log('WindowNostr: Auth state updated:', this.authState?.method);
|
||||||
@@ -1270,6 +1489,12 @@ class WindowNostr {
|
|||||||
this.authState = null;
|
this.authState = null;
|
||||||
this.authenticatedExtension = null;
|
this.authenticatedExtension = null;
|
||||||
console.log('WindowNostr: Auth state cleared');
|
console.log('WindowNostr: Auth state cleared');
|
||||||
|
|
||||||
|
// Re-install facade after logout to ensure we maintain control
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('WindowNostr: Re-installing facade after logout');
|
||||||
|
window.nostr = this;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* Two-file architecture:
|
* Two-file architecture:
|
||||||
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
||||||
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
|
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
|
||||||
* Generated on: 2025-09-15T17:48:16.817Z
|
* Generated on: 2025-09-16T15:52:30.145Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Verify dependencies are loaded
|
// Verify dependencies are loaded
|
||||||
@@ -1076,26 +1076,6 @@ class Modal {
|
|||||||
button.style.background = 'var(--nl-secondary-color)';
|
button.style.background = 'var(--nl-secondary-color)';
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
iconDiv.style.cssText = `
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 16px;
|
|
||||||
width: 50px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--nl-primary-color);
|
|
||||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.style.cssText = 'flex: 1; text-align: left;';
|
contentDiv.style.cssText = 'flex: 1; text-align: left;';
|
||||||
|
|
||||||
@@ -1119,7 +1099,6 @@ class Modal {
|
|||||||
contentDiv.appendChild(titleDiv);
|
contentDiv.appendChild(titleDiv);
|
||||||
contentDiv.appendChild(descDiv);
|
contentDiv.appendChild(descDiv);
|
||||||
|
|
||||||
button.appendChild(iconDiv);
|
|
||||||
button.appendChild(contentDiv);
|
button.appendChild(contentDiv);
|
||||||
this.modalBody.appendChild(button);
|
this.modalBody.appendChild(button);
|
||||||
});
|
});
|
||||||
@@ -1149,23 +1128,62 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleExtension() {
|
_handleExtension() {
|
||||||
// Detect all available real extensions
|
// SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension
|
||||||
const availableExtensions = this._detectAllExtensions();
|
let extension = null;
|
||||||
|
|
||||||
console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName));
|
// Check if NostrLite instance has a preserved extension (real extension detected at init)
|
||||||
|
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||||
if (availableExtensions.length === 0) {
|
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||||
console.log('Modal: No real extensions found');
|
console.log('Modal: Using preserved extension:', extension.constructor?.name);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
// 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() {
|
_detectAllExtensions() {
|
||||||
@@ -1211,17 +1229,38 @@ class Modal {
|
|||||||
|
|
||||||
// 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 && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
|
|
||||||
extensions.push({
|
if (window.nostr) {
|
||||||
name: 'window.nostr',
|
// Check if window.nostr is our WindowNostr facade with a preserved extension
|
||||||
displayName: 'Extension (window.nostr)',
|
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
|
||||||
icon: '🔑',
|
console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
|
||||||
extension: window.nostr
|
const preservedExtension = window.nostr.existingNostr;
|
||||||
});
|
console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
|
||||||
seenExtensions.add(window.nostr);
|
|
||||||
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
|
||||||
} else if (window.nostr) {
|
extensions.push({
|
||||||
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`);
|
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)',
|
||||||
|
icon: '🔑',
|
||||||
|
extension: window.nostr
|
||||||
|
});
|
||||||
|
seenExtensions.add(window.nostr);
|
||||||
|
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
@@ -1627,15 +1666,176 @@ class Modal {
|
|||||||
|
|
||||||
const warningDiv = document.createElement('div');
|
const warningDiv = document.createElement('div');
|
||||||
warningDiv.textContent = '⚠️ Save your secret key securely!';
|
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');
|
// Helper function to create copy button
|
||||||
nsecDiv.innerHTML = `<strong>Your Secret Key:</strong><br><code style="word-break: break-all; background: #f3f4f6; padding: 8px; border-radius: 4px;">${nsec}</code>`;
|
const createCopyButton = (text, label) => {
|
||||||
nsecDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
|
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');
|
// Convert pubkey to hex for verification
|
||||||
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>`;
|
const pubkeyHex = typeof pubkey === 'string' ? pubkey : Array.from(pubkey).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
npubDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
|
|
||||||
|
// 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');
|
const continueButton = document.createElement('button');
|
||||||
continueButton.textContent = 'Continue';
|
continueButton.textContent = 'Continue';
|
||||||
@@ -1644,12 +1844,69 @@ class Modal {
|
|||||||
|
|
||||||
this.modalBody.appendChild(title);
|
this.modalBody.appendChild(title);
|
||||||
this.modalBody.appendChild(warningDiv);
|
this.modalBody.appendChild(warningDiv);
|
||||||
this.modalBody.appendChild(nsecDiv);
|
this.modalBody.appendChild(nsecSection);
|
||||||
this.modalBody.appendChild(npubDiv);
|
this.modalBody.appendChild(npubSection);
|
||||||
this.modalBody.appendChild(continueButton);
|
this.modalBody.appendChild(continueButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setAuthMethod(method, options = {}) {
|
_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
|
// Emit auth method selection
|
||||||
const event = new CustomEvent('nlMethodSelected', {
|
const event = new CustomEvent('nlMethodSelected', {
|
||||||
detail: { method, ...options }
|
detail: { method, ...options }
|
||||||
@@ -1683,8 +1940,13 @@ class Modal {
|
|||||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||||
|
|
||||||
const message = document.createElement('p');
|
const message = document.createElement('p');
|
||||||
message.textContent = 'Please install a Nostr browser extension like Alby or getflattr and refresh the page.';
|
message.innerHTML = `
|
||||||
message.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
|
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');
|
const backButton = document.createElement('button');
|
||||||
backButton.textContent = 'Back';
|
backButton.textContent = 'Back';
|
||||||
@@ -1727,27 +1989,11 @@ class Modal {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const urlLabel = document.createElement('label');
|
// Users will enter the complete bunker connection string with relay info
|
||||||
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
|
|
||||||
|
|
||||||
const connectButton = document.createElement('button');
|
const connectButton = document.createElement('button');
|
||||||
connectButton.textContent = 'Connect to Bunker';
|
connectButton.textContent = 'Connect to Bunker';
|
||||||
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, urlInput.value);
|
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value);
|
||||||
connectButton.style.cssText = this._getButtonStyle();
|
connectButton.style.cssText = this._getButtonStyle();
|
||||||
|
|
||||||
const backButton = document.createElement('button');
|
const backButton = document.createElement('button');
|
||||||
@@ -1757,8 +2003,6 @@ class Modal {
|
|||||||
|
|
||||||
formGroup.appendChild(label);
|
formGroup.appendChild(label);
|
||||||
formGroup.appendChild(pubkeyInput);
|
formGroup.appendChild(pubkeyInput);
|
||||||
formGroup.appendChild(urlLabel);
|
|
||||||
formGroup.appendChild(urlInput);
|
|
||||||
|
|
||||||
this.modalBody.appendChild(title);
|
this.modalBody.appendChild(title);
|
||||||
this.modalBody.appendChild(description);
|
this.modalBody.appendChild(description);
|
||||||
@@ -1767,17 +2011,17 @@ class Modal {
|
|||||||
this.modalBody.appendChild(backButton);
|
this.modalBody.appendChild(backButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleNip46Connect(bunkerPubkey, bunkerUrl) {
|
_handleNip46Connect(bunkerPubkey) {
|
||||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||||
this._showError('Bunker pubkey is required');
|
this._showError('Bunker pubkey is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._showNip46Connecting(bunkerPubkey, bunkerUrl);
|
this._showNip46Connecting(bunkerPubkey);
|
||||||
this._performNip46Connect(bunkerPubkey, bunkerUrl);
|
this._performNip46Connect(bunkerPubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
_showNip46Connecting(bunkerPubkey, bunkerUrl) {
|
_showNip46Connecting(bunkerPubkey) {
|
||||||
this.modalBody.innerHTML = '';
|
this.modalBody.innerHTML = '';
|
||||||
|
|
||||||
const title = document.createElement('h3');
|
const title = document.createElement('h3');
|
||||||
@@ -1795,9 +2039,8 @@ class Modal {
|
|||||||
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
||||||
bunkerInfo.innerHTML = `
|
bunkerInfo.innerHTML = `
|
||||||
<strong>Connecting to bunker:</strong><br>
|
<strong>Connecting to bunker:</strong><br>
|
||||||
Pubkey: <code style="word-break: break-all;">${displayPubkey}</code><br>
|
Connection: <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;">Connection string contains all necessary relay information.</small>
|
||||||
<small style="color: #6b7280;">If this relay is offline, the bunker server may be unavailable.</small>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const connectingDiv = document.createElement('div');
|
const connectingDiv = document.createElement('div');
|
||||||
@@ -1814,9 +2057,9 @@ class Modal {
|
|||||||
this.modalBody.appendChild(connectingDiv);
|
this.modalBody.appendChild(connectingDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _performNip46Connect(bunkerPubkey, bunkerUrl) {
|
async _performNip46Connect(bunkerPubkey) {
|
||||||
try {
|
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
|
// Check if nostr-tools NIP-46 is available
|
||||||
if (!window.NostrTools?.nip46) {
|
if (!window.NostrTools?.nip46) {
|
||||||
@@ -1983,7 +2226,7 @@ class FloatingTab {
|
|||||||
appearance: {
|
appearance: {
|
||||||
style: 'pill', // 'pill', 'square', 'circle'
|
style: 'pill', // 'pill', 'square', 'circle'
|
||||||
theme: 'auto', // 'auto', 'light', 'dark'
|
theme: 'auto', // 'auto', 'light', 'dark'
|
||||||
icon: '[LOGIN]',
|
icon: '',
|
||||||
text: 'Login',
|
text: 'Login',
|
||||||
iconOnly: false
|
iconOnly: false
|
||||||
},
|
},
|
||||||
@@ -1993,11 +2236,14 @@ class FloatingTab {
|
|||||||
autoSlide: true,
|
autoSlide: true,
|
||||||
persistent: false
|
persistent: false
|
||||||
},
|
},
|
||||||
|
getUserInfo: false,
|
||||||
|
getUserRelay: [],
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isAuthenticated = false;
|
this.isAuthenticated = false;
|
||||||
this.userInfo = null;
|
this.userInfo = null;
|
||||||
|
this.userProfile = null;
|
||||||
this.container = null;
|
this.container = null;
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
|
|
||||||
@@ -2098,11 +2344,24 @@ class FloatingTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleAuth(authData) {
|
async _handleAuth(authData) {
|
||||||
console.log('FloatingTab: Handling authentication:', authData);
|
console.log('FloatingTab: Handling authentication:', authData);
|
||||||
this.isAuthenticated = true;
|
this.isAuthenticated = true;
|
||||||
this.userInfo = authData;
|
this.userInfo = authData;
|
||||||
|
|
||||||
|
// Fetch user profile if enabled and we have a pubkey
|
||||||
|
if (this.options.getUserInfo && authData.pubkey) {
|
||||||
|
console.log('FloatingTab: Fetching user profile for:', authData.pubkey);
|
||||||
|
try {
|
||||||
|
const profile = await this._fetchUserProfile(authData.pubkey);
|
||||||
|
this.userProfile = profile;
|
||||||
|
console.log('FloatingTab: User profile fetched:', profile);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('FloatingTab: Failed to fetch user profile:', error);
|
||||||
|
this.userProfile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options.behavior.hideWhenAuthenticated) {
|
if (this.options.behavior.hideWhenAuthenticated) {
|
||||||
this.hide();
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
@@ -2114,6 +2373,7 @@ class FloatingTab {
|
|||||||
console.log('FloatingTab: Handling logout');
|
console.log('FloatingTab: Handling logout');
|
||||||
this.isAuthenticated = false;
|
this.isAuthenticated = false;
|
||||||
this.userInfo = null;
|
this.userInfo = null;
|
||||||
|
this.userProfile = null;
|
||||||
|
|
||||||
if (this.options.behavior.hideWhenAuthenticated) {
|
if (this.options.behavior.hideWhenAuthenticated) {
|
||||||
this.show();
|
this.show();
|
||||||
@@ -2181,18 +2441,29 @@ class FloatingTab {
|
|||||||
|
|
||||||
// Update content
|
// Update content
|
||||||
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
|
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
|
||||||
const display = this.userInfo?.pubkey ?
|
let display;
|
||||||
(this.options.appearance.iconOnly ?
|
|
||||||
'[USER]' :
|
// Use profile name if available, otherwise fall back to pubkey
|
||||||
`[USER] ${this.userInfo.pubkey.slice(0, 6)}...`) :
|
if (this.userProfile?.name || this.userProfile?.display_name) {
|
||||||
(this.options.appearance.iconOnly ? '[AUTH]' : '[AUTH] Logged In');
|
const userName = this.userProfile.name || this.userProfile.display_name;
|
||||||
|
display = this.options.appearance.iconOnly
|
||||||
|
? userName.slice(0, 8)
|
||||||
|
: userName;
|
||||||
|
} else if (this.userInfo?.pubkey) {
|
||||||
|
// Fallback to pubkey display
|
||||||
|
display = this.options.appearance.iconOnly
|
||||||
|
? this.userInfo.pubkey.slice(0, 6)
|
||||||
|
: `${this.userInfo.pubkey.slice(0, 6)}...`;
|
||||||
|
} else {
|
||||||
|
display = this.options.appearance.iconOnly ? 'User' : 'Authenticated';
|
||||||
|
}
|
||||||
|
|
||||||
this.container.textContent = display;
|
this.container.textContent = display;
|
||||||
this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
|
this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
|
||||||
} else {
|
} else {
|
||||||
const display = this.options.appearance.iconOnly ?
|
const display = this.options.appearance.iconOnly ?
|
||||||
this.options.appearance.icon :
|
this.options.appearance.icon :
|
||||||
`${this.options.appearance.icon} ${this.options.appearance.text}`;
|
(this.options.appearance.icon ? `${this.options.appearance.icon} ${this.options.appearance.text}` : this.options.appearance.text);
|
||||||
|
|
||||||
this.container.textContent = display;
|
this.container.textContent = display;
|
||||||
this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
|
this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
|
||||||
@@ -2223,6 +2494,62 @@ class FloatingTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _fetchUserProfile(pubkey) {
|
||||||
|
if (!this.options.getUserInfo) {
|
||||||
|
console.log('FloatingTab: getUserInfo disabled, skipping profile fetch');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which relays to use
|
||||||
|
const relays = this.options.getUserRelay.length > 0
|
||||||
|
? this.options.getUserRelay
|
||||||
|
: (this.modal?.options?.relays || ['wss://relay.damus.io', 'wss://nos.lol']);
|
||||||
|
|
||||||
|
console.log('FloatingTab: Fetching profile from relays:', relays);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a SimplePool instance for querying
|
||||||
|
const pool = new window.NostrTools.nip46.SimplePool();
|
||||||
|
|
||||||
|
// Query for kind 0 (user metadata) events
|
||||||
|
const events = await pool.querySync(relays, {
|
||||||
|
kinds: [0],
|
||||||
|
authors: [pubkey],
|
||||||
|
limit: 1
|
||||||
|
}, { timeout: 5000 });
|
||||||
|
|
||||||
|
console.log('FloatingTab: Profile query returned', events.length, 'events');
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
console.log('FloatingTab: No profile events found');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recent event
|
||||||
|
const latestEvent = events.sort((a, b) => b.created_at - a.created_at)[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(latestEvent.content);
|
||||||
|
console.log('FloatingTab: Parsed profile:', profile);
|
||||||
|
|
||||||
|
// Return relevant profile fields
|
||||||
|
return {
|
||||||
|
name: profile.name || null,
|
||||||
|
display_name: profile.display_name || null,
|
||||||
|
about: profile.about || null,
|
||||||
|
picture: profile.picture || null,
|
||||||
|
nip05: profile.nip05 || null
|
||||||
|
};
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('FloatingTab: Failed to parse profile JSON:', parseError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FloatingTab: Profile fetch error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_position() {
|
_position() {
|
||||||
if (!this.container) return;
|
if (!this.container) return;
|
||||||
|
|
||||||
@@ -2384,7 +2711,7 @@ class NostrLite {
|
|||||||
appearance: {
|
appearance: {
|
||||||
style: 'pill',
|
style: 'pill',
|
||||||
theme: 'auto',
|
theme: 'auto',
|
||||||
icon: '[LOGIN]',
|
icon: '',
|
||||||
text: 'Login',
|
text: 'Login',
|
||||||
iconOnly: false
|
iconOnly: false
|
||||||
},
|
},
|
||||||
@@ -2393,7 +2720,9 @@ class NostrLite {
|
|||||||
showUserInfo: true,
|
showUserInfo: true,
|
||||||
autoSlide: true,
|
autoSlide: true,
|
||||||
persistent: false
|
persistent: false
|
||||||
}
|
},
|
||||||
|
getUserInfo: false,
|
||||||
|
getUserRelay: []
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
@@ -2422,16 +2751,149 @@ class NostrLite {
|
|||||||
|
|
||||||
_setupWindowNostrFacade() {
|
_setupWindowNostrFacade() {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === TRUE SINGLE-EXTENSION ARCHITECTURE ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Initial window.nostr:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Initial window.nostr constructor:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
// Store existing window.nostr if it exists (from extensions)
|
// Store existing window.nostr if it exists (from extensions)
|
||||||
const existingNostr = window.nostr;
|
const existingNostr = window.nostr;
|
||||||
|
|
||||||
// Always install our facade
|
// TRUE SINGLE-EXTENSION ARCHITECTURE: Don't install facade when extensions detected
|
||||||
window.nostr = new WindowNostr(this, existingNostr);
|
if (this._isRealExtension(existingNostr)) {
|
||||||
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed',
|
console.log('NOSTR_LOGIN_LITE: ✓ REAL EXTENSION DETECTED IMMEDIATELY - PRESERVING WITHOUT FACADE');
|
||||||
existingNostr ? '(with extension passthrough)' : '(no existing extension)');
|
console.log('NOSTR_LOGIN_LITE: Extension constructor:', existingNostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(existingNostr));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility');
|
||||||
|
this.preservedExtension = existingNostr;
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
// DON'T install facade - leave window.nostr as the extension
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEFERRED EXTENSION DETECTION: Extensions like nos2x may load after us
|
||||||
|
console.log('NOSTR_LOGIN_LITE: No real extension detected initially, starting deferred detection...');
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
|
||||||
|
let checkCount = 0;
|
||||||
|
const maxChecks = 10; // Check for up to 2 seconds
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
checkCount++;
|
||||||
|
const currentNostr = window.nostr;
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === DEFERRED CHECK ' + checkCount + '/' + maxChecks + ' ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Current window.nostr:', currentNostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Constructor:', currentNostr?.constructor?.name);
|
||||||
|
|
||||||
|
// Skip if it's our facade
|
||||||
|
if (currentNostr?.constructor?.name === 'WindowNostr') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Skipping - this is our facade');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isRealExtension(currentNostr)) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✓✓✓ LATE EXTENSION DETECTED - PRESERVING WITHOUT FACADE ✓✓✓');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension detected after ' + (checkCount * 200) + 'ms!');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension constructor:', currentNostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(currentNostr));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility');
|
||||||
|
this.preservedExtension = currentNostr;
|
||||||
|
this.facadeInstalled = false;
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
// DON'T install facade - leave window.nostr as the extension
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop checking after max attempts - no extension found
|
||||||
|
if (checkCount >= maxChecks) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ⚠️ MAX CHECKS REACHED - NO EXTENSION FOUND');
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Installing facade for local/NIP-46/readonly methods');
|
||||||
|
this._installFacade();
|
||||||
|
}
|
||||||
|
}, 200); // Check every 200ms
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Waiting for deferred detection to complete...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_installFacade(existingNostr = null) {
|
||||||
|
if (typeof window !== 'undefined' && !this.facadeInstalled) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === _installFacade CALLED ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name);
|
||||||
|
|
||||||
|
const facade = new WindowNostr(this, existingNostr);
|
||||||
|
window.nostr = facade;
|
||||||
|
this.facadeInstalled = true;
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === FACADE INSTALLED WITH EXTENSION ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to identify real browser extensions
|
||||||
|
_isRealExtension(obj) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: === _isRealExtension DEBUG ===');
|
||||||
|
console.log('NOSTR_LOGIN_LITE: obj:', obj);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj);
|
||||||
|
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Not an object');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Object keys:', Object.keys(obj));
|
||||||
|
console.log('NOSTR_LOGIN_LITE: getPublicKey type:', typeof obj.getPublicKey);
|
||||||
|
console.log('NOSTR_LOGIN_LITE: signEvent type:', typeof obj.signEvent);
|
||||||
|
|
||||||
|
// Must have required Nostr methods
|
||||||
|
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Missing required methods');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude our own library classes
|
||||||
|
const constructorName = obj.constructor?.name;
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName);
|
||||||
|
|
||||||
|
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Is our library class');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude NostrTools library object
|
||||||
|
if (obj === window.NostrTools) {
|
||||||
|
console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real extensions typically have internal properties or specific characteristics
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension property check:');
|
||||||
|
console.log(' _isEnabled:', !!obj._isEnabled);
|
||||||
|
console.log(' enabled:', !!obj.enabled);
|
||||||
|
console.log(' kind:', !!obj.kind);
|
||||||
|
console.log(' _eventEmitter:', !!obj._eventEmitter);
|
||||||
|
console.log(' _scope:', !!obj._scope);
|
||||||
|
console.log(' _requests:', !!obj._requests);
|
||||||
|
console.log(' _pubkey:', !!obj._pubkey);
|
||||||
|
console.log(' name:', !!obj.name);
|
||||||
|
console.log(' version:', !!obj.version);
|
||||||
|
console.log(' description:', !!obj.description);
|
||||||
|
|
||||||
|
const hasExtensionProps = !!(
|
||||||
|
obj._isEnabled || obj.enabled || obj.kind ||
|
||||||
|
obj._eventEmitter || obj._scope || obj._requests || obj._pubkey ||
|
||||||
|
obj.name || obj.version || obj.description
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('NOSTR_LOGIN_LITE: Extension detection result for', constructorName, ':', hasExtensionProps);
|
||||||
|
return hasExtensionProps;
|
||||||
|
}
|
||||||
|
|
||||||
launch(startScreen = 'login') {
|
launch(startScreen = 'login') {
|
||||||
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
||||||
|
|
||||||
@@ -2562,13 +3024,13 @@ class WindowNostr {
|
|||||||
if (event.detail.method === 'extension') {
|
if (event.detail.method === 'extension') {
|
||||||
this.authenticatedExtension = event.detail.extension;
|
this.authenticatedExtension = event.detail.extension;
|
||||||
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
|
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
|
||||||
|
}
|
||||||
// Re-install our facade to ensure we intercept signEvent calls
|
|
||||||
// Extensions may overwrite window.nostr after authentication
|
// CRITICAL FIX: Re-install our facade for ALL authentication methods
|
||||||
if (typeof window !== 'undefined') {
|
// Extensions may overwrite window.nostr after ANY authentication, not just extension auth
|
||||||
console.log('WindowNostr: Re-installing facade after authentication');
|
if (typeof window !== 'undefined') {
|
||||||
window.nostr = this;
|
console.log('WindowNostr: Re-installing facade after', this.authState?.method, 'authentication');
|
||||||
}
|
window.nostr = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('WindowNostr: Auth state updated:', this.authState?.method);
|
console.log('WindowNostr: Auth state updated:', this.authState?.method);
|
||||||
@@ -2578,6 +3040,12 @@ class WindowNostr {
|
|||||||
this.authState = null;
|
this.authState = null;
|
||||||
this.authenticatedExtension = null;
|
this.authenticatedExtension = null;
|
||||||
console.log('WindowNostr: Auth state cleared');
|
console.log('WindowNostr: Auth state cleared');
|
||||||
|
|
||||||
|
// Re-install facade after logout to ensure we maintain control
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('WindowNostr: Re-installing facade after logout');
|
||||||
|
window.nostr = this;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
417
lite/ui/modal.js
417
lite/ui/modal.js
@@ -280,26 +280,6 @@ class Modal {
|
|||||||
button.style.background = 'var(--nl-secondary-color)';
|
button.style.background = 'var(--nl-secondary-color)';
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
iconDiv.style.cssText = `
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 16px;
|
|
||||||
width: 50px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--nl-primary-color);
|
|
||||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.style.cssText = 'flex: 1; text-align: left;';
|
contentDiv.style.cssText = 'flex: 1; text-align: left;';
|
||||||
|
|
||||||
@@ -323,7 +303,6 @@ class Modal {
|
|||||||
contentDiv.appendChild(titleDiv);
|
contentDiv.appendChild(titleDiv);
|
||||||
contentDiv.appendChild(descDiv);
|
contentDiv.appendChild(descDiv);
|
||||||
|
|
||||||
button.appendChild(iconDiv);
|
|
||||||
button.appendChild(contentDiv);
|
button.appendChild(contentDiv);
|
||||||
this.modalBody.appendChild(button);
|
this.modalBody.appendChild(button);
|
||||||
});
|
});
|
||||||
@@ -353,23 +332,62 @@ class Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleExtension() {
|
_handleExtension() {
|
||||||
// Detect all available real extensions
|
// SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension
|
||||||
const availableExtensions = this._detectAllExtensions();
|
let extension = null;
|
||||||
|
|
||||||
console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName));
|
// Check if NostrLite instance has a preserved extension (real extension detected at init)
|
||||||
|
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||||
if (availableExtensions.length === 0) {
|
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||||
console.log('Modal: No real extensions found');
|
console.log('Modal: Using preserved extension:', extension.constructor?.name);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
// 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() {
|
_detectAllExtensions() {
|
||||||
@@ -415,17 +433,38 @@ class Modal {
|
|||||||
|
|
||||||
// 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 && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
|
|
||||||
extensions.push({
|
if (window.nostr) {
|
||||||
name: 'window.nostr',
|
// Check if window.nostr is our WindowNostr facade with a preserved extension
|
||||||
displayName: 'Extension (window.nostr)',
|
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
|
||||||
icon: '🔑',
|
console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
|
||||||
extension: window.nostr
|
const preservedExtension = window.nostr.existingNostr;
|
||||||
});
|
console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
|
||||||
seenExtensions.add(window.nostr);
|
|
||||||
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
|
||||||
} else if (window.nostr) {
|
extensions.push({
|
||||||
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`);
|
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)',
|
||||||
|
icon: '🔑',
|
||||||
|
extension: window.nostr
|
||||||
|
});
|
||||||
|
seenExtensions.add(window.nostr);
|
||||||
|
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
@@ -831,15 +870,176 @@ class Modal {
|
|||||||
|
|
||||||
const warningDiv = document.createElement('div');
|
const warningDiv = document.createElement('div');
|
||||||
warningDiv.textContent = '⚠️ Save your secret key securely!';
|
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');
|
// Helper function to create copy button
|
||||||
nsecDiv.innerHTML = `<strong>Your Secret Key:</strong><br><code style="word-break: break-all; background: #f3f4f6; padding: 8px; border-radius: 4px;">${nsec}</code>`;
|
const createCopyButton = (text, label) => {
|
||||||
nsecDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
|
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');
|
// Convert pubkey to hex for verification
|
||||||
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>`;
|
const pubkeyHex = typeof pubkey === 'string' ? pubkey : Array.from(pubkey).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
npubDiv.style.cssText = 'margin-bottom: 16px; font-size: 14px;';
|
|
||||||
|
// 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');
|
const continueButton = document.createElement('button');
|
||||||
continueButton.textContent = 'Continue';
|
continueButton.textContent = 'Continue';
|
||||||
@@ -848,12 +1048,69 @@ class Modal {
|
|||||||
|
|
||||||
this.modalBody.appendChild(title);
|
this.modalBody.appendChild(title);
|
||||||
this.modalBody.appendChild(warningDiv);
|
this.modalBody.appendChild(warningDiv);
|
||||||
this.modalBody.appendChild(nsecDiv);
|
this.modalBody.appendChild(nsecSection);
|
||||||
this.modalBody.appendChild(npubDiv);
|
this.modalBody.appendChild(npubSection);
|
||||||
this.modalBody.appendChild(continueButton);
|
this.modalBody.appendChild(continueButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setAuthMethod(method, options = {}) {
|
_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
|
// Emit auth method selection
|
||||||
const event = new CustomEvent('nlMethodSelected', {
|
const event = new CustomEvent('nlMethodSelected', {
|
||||||
detail: { method, ...options }
|
detail: { method, ...options }
|
||||||
@@ -887,8 +1144,13 @@ class Modal {
|
|||||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||||
|
|
||||||
const message = document.createElement('p');
|
const message = document.createElement('p');
|
||||||
message.textContent = 'Please install a Nostr browser extension like Alby or getflattr and refresh the page.';
|
message.innerHTML = `
|
||||||
message.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
|
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');
|
const backButton = document.createElement('button');
|
||||||
backButton.textContent = 'Back';
|
backButton.textContent = 'Back';
|
||||||
@@ -931,27 +1193,11 @@ class Modal {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const urlLabel = document.createElement('label');
|
// Users will enter the complete bunker connection string with relay info
|
||||||
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
|
|
||||||
|
|
||||||
const connectButton = document.createElement('button');
|
const connectButton = document.createElement('button');
|
||||||
connectButton.textContent = 'Connect to Bunker';
|
connectButton.textContent = 'Connect to Bunker';
|
||||||
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, urlInput.value);
|
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value);
|
||||||
connectButton.style.cssText = this._getButtonStyle();
|
connectButton.style.cssText = this._getButtonStyle();
|
||||||
|
|
||||||
const backButton = document.createElement('button');
|
const backButton = document.createElement('button');
|
||||||
@@ -961,8 +1207,6 @@ class Modal {
|
|||||||
|
|
||||||
formGroup.appendChild(label);
|
formGroup.appendChild(label);
|
||||||
formGroup.appendChild(pubkeyInput);
|
formGroup.appendChild(pubkeyInput);
|
||||||
formGroup.appendChild(urlLabel);
|
|
||||||
formGroup.appendChild(urlInput);
|
|
||||||
|
|
||||||
this.modalBody.appendChild(title);
|
this.modalBody.appendChild(title);
|
||||||
this.modalBody.appendChild(description);
|
this.modalBody.appendChild(description);
|
||||||
@@ -971,17 +1215,17 @@ class Modal {
|
|||||||
this.modalBody.appendChild(backButton);
|
this.modalBody.appendChild(backButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleNip46Connect(bunkerPubkey, bunkerUrl) {
|
_handleNip46Connect(bunkerPubkey) {
|
||||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||||
this._showError('Bunker pubkey is required');
|
this._showError('Bunker pubkey is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._showNip46Connecting(bunkerPubkey, bunkerUrl);
|
this._showNip46Connecting(bunkerPubkey);
|
||||||
this._performNip46Connect(bunkerPubkey, bunkerUrl);
|
this._performNip46Connect(bunkerPubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
_showNip46Connecting(bunkerPubkey, bunkerUrl) {
|
_showNip46Connecting(bunkerPubkey) {
|
||||||
this.modalBody.innerHTML = '';
|
this.modalBody.innerHTML = '';
|
||||||
|
|
||||||
const title = document.createElement('h3');
|
const title = document.createElement('h3');
|
||||||
@@ -999,9 +1243,8 @@ class Modal {
|
|||||||
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
||||||
bunkerInfo.innerHTML = `
|
bunkerInfo.innerHTML = `
|
||||||
<strong>Connecting to bunker:</strong><br>
|
<strong>Connecting to bunker:</strong><br>
|
||||||
Pubkey: <code style="word-break: break-all;">${displayPubkey}</code><br>
|
Connection: <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;">Connection string contains all necessary relay information.</small>
|
||||||
<small style="color: #6b7280;">If this relay is offline, the bunker server may be unavailable.</small>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const connectingDiv = document.createElement('div');
|
const connectingDiv = document.createElement('div');
|
||||||
@@ -1018,9 +1261,9 @@ class Modal {
|
|||||||
this.modalBody.appendChild(connectingDiv);
|
this.modalBody.appendChild(connectingDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _performNip46Connect(bunkerPubkey, bunkerUrl) {
|
async _performNip46Connect(bunkerPubkey) {
|
||||||
try {
|
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
|
// Check if nostr-tools NIP-46 is available
|
||||||
if (!window.NostrTools?.nip46) {
|
if (!window.NostrTools?.nip46) {
|
||||||
|
|||||||
Reference in New Issue
Block a user