Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca75df8bb4 | ||
|
|
c747f1f315 | ||
|
|
2a66b5eeec | ||
|
|
fa9688b17e | ||
|
|
a0e18c34d6 | ||
|
|
995c3f526c |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -18,10 +18,4 @@ Thumbs.db
|
||||
log.txt
|
||||
Trash/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Aider files
|
||||
.aider.chat.history.md
|
||||
.aider.input.history
|
||||
.aider.tags.cache.v3/
|
||||
nostr-login/
|
||||
@@ -24,6 +24,11 @@ await NOSTR_LOGIN_LITE.init({
|
||||
enabled: true,
|
||||
hPosition: 0.95, // 0.0-1.0 or '95%' from left
|
||||
vPosition: 0.5, // 0.0-1.0 or '50%' from top
|
||||
getUserInfo: true, // Fetch user profile name from relays
|
||||
getUserRelay: [ // Relays for profile queries
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol'
|
||||
],
|
||||
appearance: {
|
||||
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
||||
theme: 'auto', // 'auto' follows main theme
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
theme: 'default',
|
||||
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
theme: 'dark',
|
||||
theme: 'default',
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
@@ -48,12 +48,11 @@
|
||||
},
|
||||
floatingTab: {
|
||||
enabled: true,
|
||||
hPosition: 0.7, // 0.0-1.0 or '95%' from left
|
||||
vPosition: 0.5, // 0.0-1.0 or '50%' from top
|
||||
hPosition: 1, // 0.0-1.0 or '95%' from left
|
||||
vPosition: 0, // 0.0-1.0 or '50%' from top
|
||||
appearance: {
|
||||
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
||||
theme: 'auto', // 'auto' follows main theme
|
||||
icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
|
||||
style: 'square', // 'pill', 'square', 'circle', 'minimal'
|
||||
// icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
|
||||
text: 'Login'
|
||||
},
|
||||
behavior: {
|
||||
|
||||
184
examples/sign.html
Normal file
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>
|
||||
674
lite/build.js
674
lite/build.js
@@ -58,509 +58,10 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓');
|
||||
console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools));
|
||||
console.log('NOSTR_LOGIN_LITE: NIP-06 available:', !!window.NostrTools.nip06);
|
||||
console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46);
|
||||
}
|
||||
|
||||
// ===== NIP-46 Extension Integration =====
|
||||
// Add NIP-46 functionality to NostrTools if not already present
|
||||
if (typeof window.NostrTools !== 'undefined' && !window.NostrTools.nip46) {
|
||||
console.log('NOSTR_LOGIN_LITE: Adding NIP-46 extension to NostrTools');
|
||||
|
||||
const { nip44, generateSecretKey, getPublicKey, finalizeEvent, verifyEvent, utils } = window.NostrTools;
|
||||
|
||||
// NIP-05 regex for parsing
|
||||
const NIP05_REGEX = /^(?:([\\w.+-]+)@)?([\\w_-]+(\.[\\w_-]+)+)$/;
|
||||
const BUNKER_REGEX = /^bunker:\\/\\/([0-9a-f]{64})\\??([?\\/\\w:.=&%-]*)$/;
|
||||
const EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
|
||||
|
||||
// Event kinds
|
||||
const NostrConnect = 24133;
|
||||
const ClientAuth = 22242;
|
||||
const Handlerinformation = 31990;
|
||||
|
||||
// Fetch implementation
|
||||
let _fetch;
|
||||
try {
|
||||
_fetch = fetch;
|
||||
} catch {
|
||||
_fetch = null;
|
||||
}
|
||||
|
||||
function useFetchImplementation(fetchImplementation) {
|
||||
_fetch = fetchImplementation;
|
||||
}
|
||||
|
||||
// Simple Pool implementation for NIP-46
|
||||
class SimplePool {
|
||||
constructor() {
|
||||
this.relays = new Map();
|
||||
this.subscriptions = new Map();
|
||||
}
|
||||
|
||||
async ensureRelay(url) {
|
||||
if (!this.relays.has(url)) {
|
||||
console.log(\`NIP-46: Connecting to relay \${url}\`);
|
||||
const ws = new WebSocket(url);
|
||||
const relay = {
|
||||
ws,
|
||||
connected: false,
|
||||
subscriptions: new Map()
|
||||
};
|
||||
|
||||
this.relays.set(url, relay);
|
||||
|
||||
// Wait for connection with proper event handlers
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error(\`NIP-46: Connection timeout for \${url}\`);
|
||||
reject(new Error(\`Connection timeout to \${url}\`));
|
||||
}, 10000); // 10 second timeout
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log(\`NIP-46: Successfully connected to relay \${url}, WebSocket state: \${ws.readyState}\`);
|
||||
relay.connected = true;
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error(\`NIP-46: Failed to connect to \${url}:\`, error);
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(\`Failed to connect to \${url}: \${error.message || 'Connection failed'}\`));
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(\`NIP-46: Disconnected from relay \${url}:\`, event.code, event.reason);
|
||||
relay.connected = false;
|
||||
if (this.relays.has(url)) {
|
||||
this.relays.delete(url);
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(\`Connection closed during setup: \${event.reason || 'Unknown reason'}\`));
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const relay = this.relays.get(url);
|
||||
// Verify the existing connection is still open
|
||||
if (!relay.connected || relay.ws.readyState !== WebSocket.OPEN) {
|
||||
console.log(\`NIP-46: Reconnecting to relay \${url}\`);
|
||||
this.relays.delete(url);
|
||||
return await this.ensureRelay(url); // Recursively reconnect
|
||||
}
|
||||
}
|
||||
|
||||
const relay = this.relays.get(url);
|
||||
console.log(\`NIP-46: Relay \${url} ready, WebSocket state: \${relay.ws.readyState}\`);
|
||||
return relay;
|
||||
}
|
||||
|
||||
subscribe(relays, filters, params = {}) {
|
||||
const subId = Math.random().toString(36).substring(7);
|
||||
|
||||
relays.forEach(async (url) => {
|
||||
try {
|
||||
const relay = await this.ensureRelay(url);
|
||||
|
||||
relay.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data[0] === 'EVENT' && data[1] === subId) {
|
||||
params.onevent?.(data[2]);
|
||||
} else if (data[0] === 'EOSE' && data[1] === subId) {
|
||||
params.oneose?.();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure filters is an array
|
||||
const filtersArray = Array.isArray(filters) ? filters : [filters];
|
||||
const reqMsg = JSON.stringify(['REQ', subId, ...filtersArray]);
|
||||
relay.ws.send(reqMsg);
|
||||
|
||||
} catch (err) {
|
||||
console.warn('Failed to connect to relay:', url, err);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
relays.forEach(async (url) => {
|
||||
const relay = this.relays.get(url);
|
||||
if (relay?.connected) {
|
||||
relay.ws.send(JSON.stringify(['CLOSE', subId]));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async publish(relays, event) {
|
||||
console.log(\`NIP-46: Publishing event to \${relays.length} relays:\`, event);
|
||||
|
||||
const promises = relays.map(async (url) => {
|
||||
try {
|
||||
console.log(\`NIP-46: Attempting to publish to \${url}\`);
|
||||
const relay = await this.ensureRelay(url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error(\`NIP-46: Publish timeout to \${url}\`);
|
||||
reject(new Error(\`Publish timeout to \${url}\`));
|
||||
}, 10000); // Increased timeout to 10 seconds
|
||||
|
||||
// Set up message handler for this specific event
|
||||
const messageHandler = (msg) => {
|
||||
try {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (data[0] === 'OK' && data[1] === event.id) {
|
||||
clearTimeout(timeout);
|
||||
relay.ws.removeEventListener('message', messageHandler);
|
||||
if (data[2]) {
|
||||
console.log(\`NIP-46: Publish success to \${url}:\`, data[3]);
|
||||
resolve(data[3]);
|
||||
} else {
|
||||
console.error(\`NIP-46: Publish rejected by \${url}:\`, data[3]);
|
||||
reject(new Error(\`Publish rejected: \${data[3]}\`));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(\`NIP-46: Error parsing message from \${url}:\`, err);
|
||||
clearTimeout(timeout);
|
||||
relay.ws.removeEventListener('message', messageHandler);
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
relay.ws.addEventListener('message', messageHandler);
|
||||
|
||||
// Double-check WebSocket state before sending
|
||||
console.log(\`NIP-46: About to publish to \${url}, WebSocket state: \${relay.ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)\`);
|
||||
if (relay.ws.readyState === WebSocket.OPEN) {
|
||||
console.log(\`NIP-46: Sending event to \${url}\`);
|
||||
relay.ws.send(JSON.stringify(['EVENT', event]));
|
||||
} else {
|
||||
console.error(\`NIP-46: WebSocket not ready for \${url}, state: \${relay.ws.readyState}\`);
|
||||
clearTimeout(timeout);
|
||||
relay.ws.removeEventListener('message', messageHandler);
|
||||
reject(new Error(\`WebSocket not ready for \${url}, state: \${relay.ws.readyState}\`));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(\`NIP-46: Failed to publish to \${url}:\`, err);
|
||||
return Promise.reject(new Error(\`Failed to publish to \${url}: \${err.message}\`));
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
console.log(\`NIP-46: Publish results:\`, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
async querySync(relays, filter, params = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const events = [];
|
||||
this.subscribe(relays, [filter], {
|
||||
...params,
|
||||
onevent: (event) => events.push(event),
|
||||
oneose: () => resolve(events)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bunker URL utilities
|
||||
function toBunkerURL(bunkerPointer) {
|
||||
let bunkerURL = new URL(\`bunker://\${bunkerPointer.pubkey}\`);
|
||||
bunkerPointer.relays.forEach((relay) => {
|
||||
bunkerURL.searchParams.append('relay', relay);
|
||||
});
|
||||
if (bunkerPointer.secret) {
|
||||
bunkerURL.searchParams.set('secret', bunkerPointer.secret);
|
||||
}
|
||||
return bunkerURL.toString();
|
||||
}
|
||||
|
||||
async function parseBunkerInput(input) {
|
||||
let match = input.match(BUNKER_REGEX);
|
||||
if (match) {
|
||||
try {
|
||||
const pubkey = match[1];
|
||||
const qs = new URLSearchParams(match[2]);
|
||||
return {
|
||||
pubkey,
|
||||
relays: qs.getAll('relay'),
|
||||
secret: qs.get('secret')
|
||||
};
|
||||
} catch (_err) {
|
||||
// Continue to NIP-05 parsing
|
||||
}
|
||||
}
|
||||
return queryBunkerProfile(input);
|
||||
}
|
||||
|
||||
async function queryBunkerProfile(nip05) {
|
||||
if (!_fetch) {
|
||||
throw new Error('Fetch implementation not available');
|
||||
}
|
||||
|
||||
const match = nip05.match(NIP05_REGEX);
|
||||
if (!match) return null;
|
||||
|
||||
const [_, name = '_', domain] = match;
|
||||
try {
|
||||
const url = \`https://\${domain}/.well-known/nostr.json?name=\${name}\`;
|
||||
const res = await (await _fetch(url, { redirect: 'error' })).json();
|
||||
let pubkey = res.names[name];
|
||||
let relays = res.nip46[pubkey] || [];
|
||||
return { pubkey, relays, secret: null };
|
||||
} catch (_err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// BunkerSigner class
|
||||
class BunkerSigner {
|
||||
constructor(clientSecretKey, bp, params = {}) {
|
||||
if (bp.relays.length === 0) {
|
||||
throw new Error('no relays are specified for this bunker');
|
||||
}
|
||||
|
||||
this.params = params;
|
||||
this.pool = params.pool || new SimplePool();
|
||||
this.secretKey = clientSecretKey;
|
||||
this.conversationKey = nip44.getConversationKey(clientSecretKey, bp.pubkey);
|
||||
this.bp = bp;
|
||||
this.isOpen = false;
|
||||
this.idPrefix = Math.random().toString(36).substring(7);
|
||||
this.serial = 0;
|
||||
this.listeners = {};
|
||||
this.waitingForAuth = {};
|
||||
this.ready = false;
|
||||
this.readyPromise = this.setupSubscription(params);
|
||||
}
|
||||
|
||||
async setupSubscription(params) {
|
||||
console.log('NIP-46: Setting up subscription to relays:', this.bp.relays);
|
||||
const listeners = this.listeners;
|
||||
const waitingForAuth = this.waitingForAuth;
|
||||
const convKey = this.conversationKey;
|
||||
|
||||
// Ensure all relays are connected first
|
||||
await Promise.all(this.bp.relays.map(url => this.pool.ensureRelay(url)));
|
||||
console.log('NIP-46: All relays connected, setting up subscription');
|
||||
|
||||
this.subCloser = this.pool.subscribe(
|
||||
this.bp.relays,
|
||||
[{ kinds: [NostrConnect], authors: [this.bp.pubkey], '#p': [getPublicKey(this.secretKey)] }],
|
||||
{
|
||||
onevent: async (event) => {
|
||||
const o = JSON.parse(nip44.decrypt(event.content, convKey));
|
||||
const { id, result, error } = o;
|
||||
|
||||
if (result === 'auth_url' && waitingForAuth[id]) {
|
||||
delete waitingForAuth[id];
|
||||
if (params.onauth) {
|
||||
params.onauth(error);
|
||||
} else {
|
||||
console.warn(
|
||||
\`NIP-46: remote signer \${this.bp.pubkey} tried to send an "auth_url"='\${error}' but there was no onauth() callback configured.\`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let handler = listeners[id];
|
||||
if (handler) {
|
||||
if (error) handler.reject(error);
|
||||
else if (result) handler.resolve(result);
|
||||
delete listeners[id];
|
||||
}
|
||||
},
|
||||
onclose: () => {
|
||||
this.subCloser = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.isOpen = true;
|
||||
this.ready = true;
|
||||
console.log('NIP-46: BunkerSigner setup complete and ready');
|
||||
}
|
||||
|
||||
async ensureReady() {
|
||||
if (!this.ready) {
|
||||
console.log('NIP-46: Waiting for BunkerSigner to be ready...');
|
||||
await this.readyPromise;
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.isOpen = false;
|
||||
this.subCloser?.close();
|
||||
}
|
||||
|
||||
async sendRequest(method, params) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await this.ensureReady(); // Wait for BunkerSigner to be ready
|
||||
|
||||
if (!this.isOpen) {
|
||||
throw new Error('this signer is not open anymore, create a new one');
|
||||
}
|
||||
if (!this.subCloser) {
|
||||
await this.setupSubscription(this.params);
|
||||
}
|
||||
|
||||
this.serial++;
|
||||
const id = \`\${this.idPrefix}-\${this.serial}\`;
|
||||
const encryptedContent = nip44.encrypt(JSON.stringify({ id, method, params }), this.conversationKey);
|
||||
|
||||
const verifiedEvent = finalizeEvent(
|
||||
{
|
||||
kind: NostrConnect,
|
||||
tags: [['p', this.bp.pubkey]],
|
||||
content: encryptedContent,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
},
|
||||
this.secretKey
|
||||
);
|
||||
|
||||
this.listeners[id] = { resolve, reject };
|
||||
this.waitingForAuth[id] = true;
|
||||
|
||||
console.log(\`NIP-46: Sending \${method} request with id \${id}\`);
|
||||
const publishResults = await this.pool.publish(this.bp.relays, verifiedEvent);
|
||||
// Check if at least one publish succeeded
|
||||
const hasSuccess = publishResults.some(result => result.status === 'fulfilled');
|
||||
if (!hasSuccess) {
|
||||
throw new Error('Failed to publish to any relay');
|
||||
}
|
||||
console.log(\`NIP-46: \${method} request sent successfully\`);
|
||||
} catch (err) {
|
||||
console.error(\`NIP-46: sendRequest \${method} failed:\`, err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ping() {
|
||||
let resp = await this.sendRequest('ping', []);
|
||||
if (resp !== 'pong') {
|
||||
throw new Error(\`result is not pong: \${resp}\`);
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await this.sendRequest('connect', [this.bp.pubkey, this.bp.secret || '']);
|
||||
}
|
||||
|
||||
async getPublicKey() {
|
||||
if (!this.cachedPubKey) {
|
||||
this.cachedPubKey = await this.sendRequest('get_public_key', []);
|
||||
}
|
||||
return this.cachedPubKey;
|
||||
}
|
||||
|
||||
async signEvent(event) {
|
||||
let resp = await this.sendRequest('sign_event', [JSON.stringify(event)]);
|
||||
let signed = JSON.parse(resp);
|
||||
if (verifyEvent(signed)) {
|
||||
return signed;
|
||||
} else {
|
||||
throw new Error(\`event returned from bunker is improperly signed: \${JSON.stringify(signed)}\`);
|
||||
}
|
||||
}
|
||||
|
||||
async nip04Encrypt(thirdPartyPubkey, plaintext) {
|
||||
return await this.sendRequest('nip04_encrypt', [thirdPartyPubkey, plaintext]);
|
||||
}
|
||||
|
||||
async nip04Decrypt(thirdPartyPubkey, ciphertext) {
|
||||
return await this.sendRequest('nip04_decrypt', [thirdPartyPubkey, ciphertext]);
|
||||
}
|
||||
|
||||
async nip44Encrypt(thirdPartyPubkey, plaintext) {
|
||||
return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, plaintext]);
|
||||
}
|
||||
|
||||
async nip44Decrypt(thirdPartyPubkey, ciphertext) {
|
||||
return await this.sendRequest('nip44_decrypt', [thirdPartyPubkey, ciphertext]);
|
||||
}
|
||||
}
|
||||
|
||||
async function createAccount(bunker, params, username, domain, email, localSecretKey = generateSecretKey()) {
|
||||
if (email && !EMAIL_REGEX.test(email)) {
|
||||
throw new Error('Invalid email');
|
||||
}
|
||||
|
||||
let rpc = new BunkerSigner(localSecretKey, bunker.bunkerPointer, params);
|
||||
let pubkey = await rpc.sendRequest('create_account', [username, domain, email || '']);
|
||||
rpc.bp.pubkey = pubkey;
|
||||
await rpc.connect();
|
||||
return rpc;
|
||||
}
|
||||
|
||||
async function fetchBunkerProviders(pool, relays) {
|
||||
const events = await pool.querySync(relays, {
|
||||
kinds: [Handlerinformation],
|
||||
'#k': [NostrConnect.toString()]
|
||||
});
|
||||
|
||||
events.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
const validatedBunkers = await Promise.all(
|
||||
events.map(async (event, i) => {
|
||||
try {
|
||||
const content = JSON.parse(event.content);
|
||||
try {
|
||||
if (events.findIndex((ev) => JSON.parse(ev.content).nip05 === content.nip05) !== i) {
|
||||
return undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue processing
|
||||
}
|
||||
|
||||
const bp = await queryBunkerProfile(content.nip05);
|
||||
if (bp && bp.pubkey === event.pubkey && bp.relays.length) {
|
||||
return {
|
||||
bunkerPointer: bp,
|
||||
nip05: content.nip05,
|
||||
domain: content.nip05.split('@')[1],
|
||||
name: content.name || content.display_name,
|
||||
picture: content.picture,
|
||||
about: content.about,
|
||||
website: content.website,
|
||||
local: false
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return validatedBunkers.filter((b) => b !== undefined);
|
||||
}
|
||||
|
||||
// Extend NostrTools with NIP-46 functionality
|
||||
window.NostrTools.nip46 = {
|
||||
BunkerSigner,
|
||||
parseBunkerInput,
|
||||
toBunkerURL,
|
||||
queryBunkerProfile,
|
||||
createAccount,
|
||||
fetchBunkerProviders,
|
||||
useFetchImplementation,
|
||||
BUNKER_REGEX,
|
||||
SimplePool
|
||||
};
|
||||
|
||||
console.log('NIP-46 extension loaded successfully');
|
||||
console.log('Available: NostrTools.nip46');
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// NOSTR_LOGIN_LITE Components
|
||||
// ======================================
|
||||
@@ -952,13 +453,13 @@ class FloatingTab {
|
||||
// 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']);
|
||||
: ['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();
|
||||
const pool = new window.NostrTools.SimplePool();
|
||||
|
||||
// Query for kind 0 (user metadata) events
|
||||
const events = await pool.querySync(relays, {
|
||||
@@ -981,9 +482,27 @@ class FloatingTab {
|
||||
const profile = JSON.parse(latestEvent.content);
|
||||
console.log('FloatingTab: Parsed profile:', profile);
|
||||
|
||||
// Return relevant profile fields
|
||||
// Find the best name from any key containing "name" (case-insensitive)
|
||||
let bestName = null;
|
||||
const nameKeys = Object.keys(profile).filter(key =>
|
||||
key.toLowerCase().includes('name') &&
|
||||
typeof profile[key] === 'string' &&
|
||||
profile[key].trim().length > 0
|
||||
);
|
||||
|
||||
if (nameKeys.length > 0) {
|
||||
// Find the shortest name value
|
||||
bestName = nameKeys
|
||||
.map(key => profile[key].trim())
|
||||
.reduce((shortest, current) =>
|
||||
current.length < shortest.length ? current : shortest
|
||||
);
|
||||
console.log('FloatingTab: Found name keys:', nameKeys, 'selected:', bestName);
|
||||
}
|
||||
|
||||
// Return relevant profile fields with the best name
|
||||
return {
|
||||
name: profile.name || null,
|
||||
name: bestName,
|
||||
display_name: profile.display_name || null,
|
||||
about: profile.about || null,
|
||||
picture: profile.picture || null,
|
||||
@@ -1144,10 +663,10 @@ class NostrLite {
|
||||
|
||||
this.options = {
|
||||
theme: 'default',
|
||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
seedphrase: false,
|
||||
readonly: true,
|
||||
connect: false,
|
||||
otp: false
|
||||
@@ -1200,16 +719,149 @@ class NostrLite {
|
||||
|
||||
_setupWindowNostrFacade() {
|
||||
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)
|
||||
const existingNostr = window.nostr;
|
||||
|
||||
// Always install our facade
|
||||
window.nostr = new WindowNostr(this, existingNostr);
|
||||
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed',
|
||||
existingNostr ? '(with extension passthrough)' : '(no existing extension)');
|
||||
// TRUE SINGLE-EXTENSION ARCHITECTURE: Don't install facade when extensions detected
|
||||
if (this._isRealExtension(existingNostr)) {
|
||||
console.log('NOSTR_LOGIN_LITE: ✓ REAL EXTENSION DETECTED IMMEDIATELY - PRESERVING WITHOUT FACADE');
|
||||
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') {
|
||||
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
||||
|
||||
@@ -1443,8 +1095,8 @@ class WindowNostr {
|
||||
}
|
||||
|
||||
async getRelays() {
|
||||
// Return configured relays from nostr-lite options
|
||||
return this.nostrLite.options?.relays || ['wss://relay.damus.io'];
|
||||
// Return default relays since we removed the relays configuration
|
||||
return ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||
}
|
||||
|
||||
get nip04() {
|
||||
|
||||
1337
lite/nostr-lite.js
1337
lite/nostr-lite.js
File diff suppressed because it is too large
Load Diff
5472
lite/nostr.bundle.js
5472
lite/nostr.bundle.js
File diff suppressed because it is too large
Load Diff
659
lite/ui/modal.js
659
lite/ui/modal.js
@@ -58,7 +58,7 @@ class Modal {
|
||||
overflow: hidden;
|
||||
`;
|
||||
} else {
|
||||
// Modal content: centered with margin
|
||||
// Modal content: centered with margin, no fixed height
|
||||
modalContent.style.cssText = `
|
||||
position: relative;
|
||||
background: var(--nl-secondary-color);
|
||||
@@ -68,7 +68,6 @@ class Modal {
|
||||
margin: 50px auto;
|
||||
border-radius: var(--nl-border-radius, 15px);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
}
|
||||
@@ -105,7 +104,7 @@ class Modal {
|
||||
closeButton.style.cssText = `
|
||||
background: var(--nl-secondary-color);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
border-radius: 4px;
|
||||
font-size: 28px;
|
||||
color: var(--nl-primary-color);
|
||||
cursor: pointer;
|
||||
@@ -133,8 +132,6 @@ class Modal {
|
||||
this.modalBody = document.createElement('div');
|
||||
this.modalBody.style.cssText = `
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
background: transparent;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
@@ -223,6 +220,16 @@ class Modal {
|
||||
});
|
||||
}
|
||||
|
||||
// Seed Phrase option - only show if explicitly enabled
|
||||
if (this.options?.methods?.seedphrase === true) {
|
||||
options.push({
|
||||
type: 'seedphrase',
|
||||
title: 'Seed Phrase',
|
||||
description: 'Import from mnemonic seed phrase',
|
||||
icon: '🌱'
|
||||
});
|
||||
}
|
||||
|
||||
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
|
||||
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
||||
options.push({
|
||||
@@ -280,6 +287,19 @@ class Modal {
|
||||
button.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
// Remove the icon entirely - no emojis or text-based icons
|
||||
iconDiv.textContent = '';
|
||||
iconDiv.style.cssText = `
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-right: 16px;
|
||||
width: 0px;
|
||||
text-align: center;
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.style.cssText = 'flex: 1; text-align: left;';
|
||||
|
||||
@@ -303,6 +323,7 @@ class Modal {
|
||||
contentDiv.appendChild(titleDiv);
|
||||
contentDiv.appendChild(descDiv);
|
||||
|
||||
button.appendChild(iconDiv);
|
||||
button.appendChild(contentDiv);
|
||||
this.modalBody.appendChild(button);
|
||||
});
|
||||
@@ -319,6 +340,9 @@ class Modal {
|
||||
case 'local':
|
||||
this._showLocalKeyScreen();
|
||||
break;
|
||||
case 'seedphrase':
|
||||
this._showSeedPhraseScreen();
|
||||
break;
|
||||
case 'connect':
|
||||
this._showConnectScreen();
|
||||
break;
|
||||
@@ -332,23 +356,62 @@ class Modal {
|
||||
}
|
||||
|
||||
_handleExtension() {
|
||||
// Detect all available real extensions
|
||||
const availableExtensions = this._detectAllExtensions();
|
||||
// SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension
|
||||
let extension = null;
|
||||
|
||||
console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName));
|
||||
|
||||
if (availableExtensions.length === 0) {
|
||||
console.log('Modal: No real extensions found');
|
||||
this._showExtensionRequired();
|
||||
} else if (availableExtensions.length === 1) {
|
||||
// Single extension - use it directly without showing choice UI
|
||||
console.log('Modal: Single extension detected, using it directly:', availableExtensions[0].displayName);
|
||||
this._tryExtensionLogin(availableExtensions[0].extension);
|
||||
} else {
|
||||
// Multiple extensions - show choice UI
|
||||
console.log('Modal: Multiple extensions detected, showing choice UI for', availableExtensions.length, 'extensions');
|
||||
this._showExtensionChoice(availableExtensions);
|
||||
// Check if NostrLite instance has a preserved extension (real extension detected at init)
|
||||
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||
console.log('Modal: Using preserved extension:', extension.constructor?.name);
|
||||
}
|
||||
// Otherwise check current window.nostr
|
||||
else if (window.nostr && this._isRealExtension(window.nostr)) {
|
||||
extension = window.nostr;
|
||||
console.log('Modal: Using current window.nostr extension:', extension.constructor?.name);
|
||||
}
|
||||
|
||||
if (!extension) {
|
||||
console.log('Modal: No extension detected yet, waiting for deferred detection...');
|
||||
|
||||
// DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10; // Try for 2 seconds
|
||||
const checkForExtension = () => {
|
||||
attempts++;
|
||||
|
||||
// Check again for preserved extension (might be set by deferred detection)
|
||||
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||||
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||||
console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name);
|
||||
this._tryExtensionLogin(extension);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current window.nostr again
|
||||
if (window.nostr && this._isRealExtension(window.nostr)) {
|
||||
extension = window.nostr;
|
||||
console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name);
|
||||
this._tryExtensionLogin(extension);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep trying or give up
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(checkForExtension, 200);
|
||||
} else {
|
||||
console.log('Modal: No browser extension found after waiting 2 seconds');
|
||||
this._showExtensionRequired();
|
||||
}
|
||||
};
|
||||
|
||||
// Start checking after a brief delay
|
||||
setTimeout(checkForExtension, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the single detected extension directly - no choice UI
|
||||
console.log('Modal: Single extension mode - using extension directly');
|
||||
this._tryExtensionLogin(extension);
|
||||
}
|
||||
|
||||
_detectAllExtensions() {
|
||||
@@ -394,17 +457,38 @@ class Modal {
|
||||
|
||||
// Also check window.nostr but be extra careful to avoid our library
|
||||
console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name);
|
||||
if (window.nostr && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
|
||||
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 if (window.nostr) {
|
||||
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`);
|
||||
|
||||
if (window.nostr) {
|
||||
// Check if window.nostr is our WindowNostr facade with a preserved extension
|
||||
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
|
||||
console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
|
||||
const preservedExtension = window.nostr.existingNostr;
|
||||
console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
|
||||
|
||||
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
|
||||
extensions.push({
|
||||
name: 'window.nostr.existingNostr',
|
||||
displayName: 'Extension (preserved by WindowNostr)',
|
||||
icon: '🔑',
|
||||
extension: preservedExtension
|
||||
});
|
||||
seenExtensions.add(preservedExtension);
|
||||
console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`);
|
||||
}
|
||||
}
|
||||
// Check if window.nostr is directly a real extension (not our facade)
|
||||
else if (this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
|
||||
extensions.push({
|
||||
name: 'window.nostr',
|
||||
displayName: 'Extension (window.nostr)',
|
||||
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;
|
||||
@@ -994,6 +1078,63 @@ class Modal {
|
||||
}
|
||||
|
||||
_setAuthMethod(method, options = {}) {
|
||||
// SINGLE-EXTENSION ARCHITECTURE: Handle method switching
|
||||
console.log('Modal: _setAuthMethod called with:', method, options);
|
||||
|
||||
// CRITICAL: Never install facade for extension methods - leave window.nostr as the extension
|
||||
if (method === 'extension') {
|
||||
console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension');
|
||||
|
||||
// Emit auth method selection directly for extension
|
||||
const event = new CustomEvent('nlMethodSelected', {
|
||||
detail: { method, ...options }
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-extension methods, we need to ensure WindowNostr facade is available
|
||||
console.log('Modal: Non-extension method detected:', method);
|
||||
|
||||
// Check if we have a preserved extension but no WindowNostr facade installed
|
||||
const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension;
|
||||
const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr';
|
||||
|
||||
console.log('Modal: Method switching check:');
|
||||
console.log(' method:', method);
|
||||
console.log(' hasPreservedExtension:', hasPreservedExtension);
|
||||
console.log(' hasWindowNostrFacade:', hasWindowNostrFacade);
|
||||
console.log(' current window.nostr constructor:', window.nostr?.constructor?.name);
|
||||
|
||||
// If we have a preserved extension but no facade, install facade for method switching
|
||||
if (hasPreservedExtension && !hasWindowNostrFacade) {
|
||||
console.log('Modal: Installing WindowNostr facade for method switching (non-extension authentication)');
|
||||
|
||||
// Get the NostrLite instance and install facade with preserved extension
|
||||
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
||||
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
||||
const preservedExtension = nostrLiteInstance.preservedExtension;
|
||||
console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name);
|
||||
|
||||
nostrLiteInstance._installFacade(preservedExtension);
|
||||
console.log('Modal: WindowNostr facade installed for method switching');
|
||||
} else {
|
||||
console.error('Modal: Cannot access NostrLite instance or _installFacade method');
|
||||
}
|
||||
}
|
||||
|
||||
// If no extension at all, ensure facade is installed for local/NIP-46/readonly methods
|
||||
else if (!hasPreservedExtension && !hasWindowNostrFacade) {
|
||||
console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)');
|
||||
|
||||
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
||||
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
||||
nostrLiteInstance._installFacade();
|
||||
console.log('Modal: WindowNostr facade installed for non-extension methods');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit auth method selection
|
||||
const event = new CustomEvent('nlMethodSelected', {
|
||||
detail: { method, ...options }
|
||||
@@ -1027,8 +1168,13 @@ class Modal {
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const message = document.createElement('p');
|
||||
message.textContent = 'Please install a Nostr browser extension like Alby or getflattr and refresh the page.';
|
||||
message.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
|
||||
message.innerHTML = `
|
||||
Please install a Nostr browser extension and refresh the page.<br><br>
|
||||
<strong>Important:</strong> If you have multiple extensions installed, please disable all but one to avoid conflicts.
|
||||
<br><br>
|
||||
Popular extensions: Alby, nos2x, Flamingo
|
||||
`;
|
||||
message.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px; line-height: 1.4;';
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
@@ -1043,10 +1189,6 @@ class Modal {
|
||||
_showConnectScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Connect to NIP-46 Remote Signer';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.';
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
@@ -1071,28 +1213,67 @@ class Modal {
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const urlLabel = document.createElement('label');
|
||||
urlLabel.textContent = 'Remote URL (optional):';
|
||||
urlLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
|
||||
|
||||
const urlInput = document.createElement('input');
|
||||
urlInput.type = 'url';
|
||||
urlInput.placeholder = 'ws://localhost:8080 (default)';
|
||||
urlInput.style.cssText = `
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Users will enter the bunker URL manually from their bunker setup
|
||||
// Add real-time bunker key validation
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const connectButton = document.createElement('button');
|
||||
connectButton.textContent = 'Connect to Bunker';
|
||||
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, urlInput.value);
|
||||
connectButton.style.cssText = this._getButtonStyle();
|
||||
connectButton.disabled = true;
|
||||
connectButton.onclick = () => {
|
||||
if (!connectButton.disabled) {
|
||||
this._handleNip46Connect(pubkeyInput.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
connectButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
pubkeyInput.oninput = () => {
|
||||
const value = pubkeyInput.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
connectButton.disabled = true;
|
||||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
connectButton.style.color = 'var(--nl-muted-color)';
|
||||
connectButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = this._validateBunkerKey(value);
|
||||
if (isValid) {
|
||||
formatHint.textContent = '✅ Valid bunker connection format detected';
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
connectButton.disabled = false;
|
||||
connectButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
connectButton.style.color = 'var(--nl-primary-color)';
|
||||
connectButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid format - must be bunker://, npub, or 64-char hex';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
connectButton.disabled = true;
|
||||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
connectButton.style.color = 'var(--nl-muted-color)';
|
||||
connectButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
@@ -1101,27 +1282,60 @@ class Modal {
|
||||
|
||||
formGroup.appendChild(label);
|
||||
formGroup.appendChild(pubkeyInput);
|
||||
formGroup.appendChild(urlLabel);
|
||||
formGroup.appendChild(urlInput);
|
||||
formGroup.appendChild(formatHint);
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(formGroup);
|
||||
this.modalBody.appendChild(connectButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_handleNip46Connect(bunkerPubkey, bunkerUrl) {
|
||||
_validateBunkerKey(bunkerKey) {
|
||||
try {
|
||||
const trimmed = bunkerKey.trim();
|
||||
|
||||
// Check for bunker:// format
|
||||
if (trimmed.startsWith('bunker://')) {
|
||||
// Should have format: bunker://pubkey or bunker://pubkey?param=value
|
||||
const match = trimmed.match(/^bunker:\/\/([0-9a-fA-F]{64})(\?.*)?$/);
|
||||
return !!match;
|
||||
}
|
||||
|
||||
// Check for npub format
|
||||
if (trimmed.startsWith('npub1') && trimmed.length === 63) {
|
||||
try {
|
||||
if (window.NostrTools?.nip19) {
|
||||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||||
return decoded.type === 'npub';
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hex format (64 characters, valid hex)
|
||||
if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.log('Bunker key validation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_handleNip46Connect(bunkerPubkey) {
|
||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||
this._showError('Bunker pubkey is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this._showNip46Connecting(bunkerPubkey, bunkerUrl);
|
||||
this._performNip46Connect(bunkerPubkey, bunkerUrl);
|
||||
this._showNip46Connecting(bunkerPubkey);
|
||||
this._performNip46Connect(bunkerPubkey);
|
||||
}
|
||||
|
||||
_showNip46Connecting(bunkerPubkey, bunkerUrl) {
|
||||
_showNip46Connecting(bunkerPubkey) {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
@@ -1139,9 +1353,8 @@ class Modal {
|
||||
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
||||
bunkerInfo.innerHTML = `
|
||||
<strong>Connecting to bunker:</strong><br>
|
||||
Pubkey: <code style="word-break: break-all;">${displayPubkey}</code><br>
|
||||
Relay: <code style="word-break: break-all;">${bunkerUrl || 'ws://localhost:8080'}</code><br>
|
||||
<small style="color: #6b7280;">If this relay is offline, the bunker server may be unavailable.</small>
|
||||
Connection: <code style="word-break: break-all;">${displayPubkey}</code><br>
|
||||
<small style="color: #6b7280;">Connection string contains all necessary relay information.</small>
|
||||
`;
|
||||
|
||||
const connectingDiv = document.createElement('div');
|
||||
@@ -1158,9 +1371,9 @@ class Modal {
|
||||
this.modalBody.appendChild(connectingDiv);
|
||||
}
|
||||
|
||||
async _performNip46Connect(bunkerPubkey, bunkerUrl) {
|
||||
async _performNip46Connect(bunkerPubkey) {
|
||||
try {
|
||||
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey, bunkerUrl);
|
||||
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
|
||||
|
||||
// Check if nostr-tools NIP-46 is available
|
||||
if (!window.NostrTools?.nip46) {
|
||||
@@ -1181,9 +1394,9 @@ class Modal {
|
||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||
console.log('Generated local client keypair for NIP-46 session');
|
||||
|
||||
// Use nostr-tools BunkerSigner constructor
|
||||
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
||||
console.log('Creating nip46 BunkerSigner...');
|
||||
const signer = new window.NostrTools.nip46.BunkerSigner(localSecretKey, bunkerPointer, {
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
onauth: (url) => {
|
||||
console.log('Received auth URL from bunker:', url);
|
||||
// Open auth URL in popup or redirect
|
||||
@@ -1260,6 +1473,312 @@ class Modal {
|
||||
this._setAuthMethod('readonly');
|
||||
}
|
||||
|
||||
_showSeedPhraseScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.innerHTML = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts, or <span id="generate-new" style="text-decoration: underline; cursor: pointer; color: var(--nl-primary-color);">generate new</span>.';
|
||||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
// Remove default placeholder text as requested
|
||||
textarea.placeholder = '';
|
||||
textarea.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
resize: none;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Add real-time mnemonic validation
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Accounts';
|
||||
importButton.disabled = true;
|
||||
importButton.onclick = () => {
|
||||
if (!importButton.disabled) {
|
||||
this._importFromSeedPhrase(textarea.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
importButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
`;
|
||||
|
||||
textarea.oninput = () => {
|
||||
const value = textarea.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = this._validateMnemonic(value);
|
||||
if (isValid) {
|
||||
const wordCount = value.split(/\s+/).length;
|
||||
formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`;
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
importButton.disabled = false;
|
||||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
importButton.style.color = 'var(--nl-primary-color)';
|
||||
importButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(textarea);
|
||||
this.modalBody.appendChild(formatHint);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
|
||||
// Add click handler for the "generate new" link
|
||||
const generateLink = document.getElementById('generate-new');
|
||||
if (generateLink) {
|
||||
generateLink.addEventListener('mouseenter', () => {
|
||||
generateLink.style.color = 'var(--nl-accent-color)';
|
||||
});
|
||||
generateLink.addEventListener('mouseleave', () => {
|
||||
generateLink.style.color = 'var(--nl-primary-color)';
|
||||
});
|
||||
generateLink.addEventListener('click', () => {
|
||||
this._generateNewSeedPhrase(textarea, formatHint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_generateNewSeedPhrase(textarea, formatHint) {
|
||||
try {
|
||||
// Check if NIP-06 is available
|
||||
if (!window.NostrTools?.nip06) {
|
||||
throw new Error('NIP-06 not available in bundle');
|
||||
}
|
||||
|
||||
// Generate a random 12-word mnemonic using NostrTools
|
||||
const mnemonic = window.NostrTools.nip06.generateSeedWords();
|
||||
|
||||
// Set the generated mnemonic in the textarea
|
||||
textarea.value = mnemonic;
|
||||
|
||||
// Trigger the oninput event to properly validate and enable the button
|
||||
if (textarea.oninput) {
|
||||
textarea.oninput();
|
||||
}
|
||||
|
||||
console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate seed phrase:', error);
|
||||
formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available';
|
||||
formatHint.style.color = '#dc2626';
|
||||
}
|
||||
}
|
||||
|
||||
_validateMnemonic(mnemonic) {
|
||||
try {
|
||||
// Check if NIP-06 is available
|
||||
if (!window.NostrTools?.nip06) {
|
||||
console.error('NIP-06 not available in bundle');
|
||||
return false;
|
||||
}
|
||||
|
||||
const words = mnemonic.trim().split(/\s+/);
|
||||
|
||||
// Must be 12 or 24 words
|
||||
if (words.length !== 12 && words.length !== 24) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to validate using NostrTools nip06 - this will throw if invalid
|
||||
window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('Mnemonic validation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_importFromSeedPhrase(mnemonic) {
|
||||
try {
|
||||
const trimmed = mnemonic.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error('Please enter a mnemonic seed phrase');
|
||||
}
|
||||
|
||||
// Validate the mnemonic
|
||||
if (!this._validateMnemonic(trimmed)) {
|
||||
throw new Error('Invalid mnemonic. Please enter a valid 12 or 24-word BIP-39 seed phrase');
|
||||
}
|
||||
|
||||
// Generate accounts 0-5 using NIP-06
|
||||
const accounts = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
try {
|
||||
const privateKey = window.NostrTools.nip06.privateKeyFromSeedWords(trimmed, '', i);
|
||||
const publicKey = window.NostrTools.getPublicKey(privateKey);
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(privateKey);
|
||||
const npub = window.NostrTools.nip19.npubEncode(publicKey);
|
||||
|
||||
accounts.push({
|
||||
index: i,
|
||||
privateKey,
|
||||
publicKey,
|
||||
nsec,
|
||||
npub
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to derive account ${i}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (accounts.length === 0) {
|
||||
throw new Error('Failed to derive any accounts from seed phrase');
|
||||
}
|
||||
|
||||
console.log(`Successfully derived ${accounts.length} accounts from seed phrase`);
|
||||
this._showAccountSelection(accounts);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Seed phrase import failed:', error);
|
||||
this._showError('Seed phrase import failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
_showAccountSelection(accounts) {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`;
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
this.modalBody.appendChild(description);
|
||||
|
||||
// Create table for account selection
|
||||
const table = document.createElement('table');
|
||||
table.style.cssText = `
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
// Table header
|
||||
const thead = document.createElement('thead');
|
||||
thead.innerHTML = `
|
||||
<tr style="background: #f3f4f6;">
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">#</th>
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Use</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
|
||||
// Table body
|
||||
const tbody = document.createElement('tbody');
|
||||
accounts.forEach(account => {
|
||||
const row = document.createElement('tr');
|
||||
row.style.cssText = 'border: 1px solid #d1d5db;';
|
||||
|
||||
const indexCell = document.createElement('td');
|
||||
indexCell.textContent = account.index;
|
||||
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
|
||||
|
||||
const actionCell = document.createElement('td');
|
||||
actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;';
|
||||
|
||||
// Show truncated npub in the button
|
||||
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||
|
||||
const selectButton = document.createElement('button');
|
||||
selectButton.textContent = truncatedNpub;
|
||||
selectButton.onclick = () => this._selectAccount(account);
|
||||
selectButton.style.cssText = `
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 11px;
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-primary-color);
|
||||
border: 1px solid var(--nl-primary-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-align: center;
|
||||
`;
|
||||
selectButton.onmouseover = () => {
|
||||
selectButton.style.borderColor = 'var(--nl-accent-color)';
|
||||
};
|
||||
selectButton.onmouseout = () => {
|
||||
selectButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
};
|
||||
|
||||
actionCell.appendChild(selectButton);
|
||||
|
||||
row.appendChild(indexCell);
|
||||
row.appendChild(actionCell);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
|
||||
this.modalBody.appendChild(table);
|
||||
|
||||
// Back button
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back to Seed Phrase';
|
||||
backButton.onclick = () => this._showSeedPhraseScreen();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary');
|
||||
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_selectAccount(account) {
|
||||
console.log('Selected account:', account.index, account.npub);
|
||||
|
||||
// Use the same auth method as local keys, but with seedphrase identifier
|
||||
this._setAuthMethod('local', {
|
||||
secret: account.nsec,
|
||||
pubkey: account.publicKey,
|
||||
source: 'seedphrase',
|
||||
accountIndex: account.index
|
||||
});
|
||||
}
|
||||
|
||||
_showOtpScreen() {
|
||||
// Placeholder for OTP functionality
|
||||
this._showError('OTP/DM not yet implemented - coming soon!');
|
||||
|
||||
1
nostr-tools
Submodule
1
nostr-tools
Submodule
Submodule nostr-tools added at 23aebbd341
Reference in New Issue
Block a user