Implement logging in via seed phrase
This commit is contained in:
509
lite/build.js
509
lite/build.js
@@ -58,509 +58,10 @@ if (typeof window !== 'undefined') {
|
|||||||
|
|
||||||
console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓');
|
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: 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);
|
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
|
// NOSTR_LOGIN_LITE Components
|
||||||
// ======================================
|
// ======================================
|
||||||
@@ -952,7 +453,7 @@ class FloatingTab {
|
|||||||
// Determine which relays to use
|
// Determine which relays to use
|
||||||
const relays = this.options.getUserRelay.length > 0
|
const relays = this.options.getUserRelay.length > 0
|
||||||
? this.options.getUserRelay
|
? 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);
|
console.log('FloatingTab: Fetching profile from relays:', relays);
|
||||||
|
|
||||||
@@ -1144,10 +645,10 @@ class NostrLite {
|
|||||||
|
|
||||||
this.options = {
|
this.options = {
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
||||||
methods: {
|
methods: {
|
||||||
extension: true,
|
extension: true,
|
||||||
local: true,
|
local: true,
|
||||||
|
seedphrase: false,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
connect: false,
|
connect: false,
|
||||||
otp: false
|
otp: false
|
||||||
@@ -1576,8 +1077,8 @@ class WindowNostr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getRelays() {
|
async getRelays() {
|
||||||
// Return configured relays from nostr-lite options
|
// Return default relays since we removed the relays configuration
|
||||||
return this.nostrLite.options?.relays || ['wss://relay.damus.io'];
|
return ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||||
}
|
}
|
||||||
|
|
||||||
get nip04() {
|
get nip04() {
|
||||||
|
|||||||
@@ -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-16T15:52:30.145Z
|
* Generated on: 2025-09-16T19:47:40.071Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Verify dependencies are loaded
|
// Verify dependencies are loaded
|
||||||
@@ -20,509 +20,10 @@ if (typeof window !== 'undefined') {
|
|||||||
|
|
||||||
console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓');
|
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: 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);
|
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
|
// NOSTR_LOGIN_LITE Components
|
||||||
// ======================================
|
// ======================================
|
||||||
@@ -854,7 +355,7 @@ class Modal {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Modal content: centered with margin
|
// Modal content: centered with margin, no fixed height
|
||||||
modalContent.style.cssText = `
|
modalContent.style.cssText = `
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--nl-secondary-color);
|
background: var(--nl-secondary-color);
|
||||||
@@ -864,7 +365,6 @@ class Modal {
|
|||||||
margin: 50px auto;
|
margin: 50px auto;
|
||||||
border-radius: var(--nl-border-radius, 15px);
|
border-radius: var(--nl-border-radius, 15px);
|
||||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||||
max-height: 600px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -929,8 +429,6 @@ class Modal {
|
|||||||
this.modalBody = document.createElement('div');
|
this.modalBody = document.createElement('div');
|
||||||
this.modalBody.style.cssText = `
|
this.modalBody.style.cssText = `
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 500px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||||
`;
|
`;
|
||||||
@@ -1019,6 +517,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)
|
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
|
||||||
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
||||||
options.push({
|
options.push({
|
||||||
@@ -1076,6 +584,27 @@ 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]',
|
||||||
|
'🌱': '[SEED]',
|
||||||
|
'🌐': '[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;';
|
||||||
|
|
||||||
@@ -1099,6 +628,7 @@ 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);
|
||||||
});
|
});
|
||||||
@@ -1115,6 +645,9 @@ class Modal {
|
|||||||
case 'local':
|
case 'local':
|
||||||
this._showLocalKeyScreen();
|
this._showLocalKeyScreen();
|
||||||
break;
|
break;
|
||||||
|
case 'seedphrase':
|
||||||
|
this._showSeedPhraseScreen();
|
||||||
|
break;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
this._showConnectScreen();
|
this._showConnectScreen();
|
||||||
break;
|
break;
|
||||||
@@ -2159,6 +1692,287 @@ class Modal {
|
|||||||
this._setAuthMethod('readonly');
|
this._setAuthMethod('readonly');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_showSeedPhraseScreen() {
|
||||||
|
this.modalBody.innerHTML = '';
|
||||||
|
|
||||||
|
const title = document.createElement('h3');
|
||||||
|
title.textContent = 'Import from Seed Phrase';
|
||||||
|
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||||
|
|
||||||
|
const description = document.createElement('p');
|
||||||
|
description.textContent = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts:';
|
||||||
|
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;';
|
||||||
|
|
||||||
|
textarea.oninput = () => {
|
||||||
|
const value = textarea.value.trim();
|
||||||
|
if (!value) {
|
||||||
|
formatHint.textContent = '';
|
||||||
|
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';
|
||||||
|
} else {
|
||||||
|
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
|
||||||
|
formatHint.style.color = '#dc2626';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate new seed phrase button
|
||||||
|
const generateButton = document.createElement('button');
|
||||||
|
generateButton.textContent = 'Generate New Seed Phrase';
|
||||||
|
generateButton.onclick = () => this._generateNewSeedPhrase(textarea, formatHint);
|
||||||
|
generateButton.style.cssText = this._getButtonStyle() + 'margin-bottom: 12px;';
|
||||||
|
|
||||||
|
const importButton = document.createElement('button');
|
||||||
|
importButton.textContent = 'Import Accounts';
|
||||||
|
importButton.onclick = () => this._importFromSeedPhrase(textarea.value);
|
||||||
|
importButton.style.cssText = this._getButtonStyle();
|
||||||
|
|
||||||
|
const backButton = document.createElement('button');
|
||||||
|
backButton.textContent = 'Back';
|
||||||
|
backButton.onclick = () => this._renderLoginOptions();
|
||||||
|
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||||
|
|
||||||
|
this.modalBody.appendChild(title);
|
||||||
|
this.modalBody.appendChild(description);
|
||||||
|
this.modalBody.appendChild(textarea);
|
||||||
|
this.modalBody.appendChild(formatHint);
|
||||||
|
this.modalBody.appendChild(generateButton);
|
||||||
|
this.modalBody.appendChild(importButton);
|
||||||
|
this.modalBody.appendChild(backButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 validation to show it's valid
|
||||||
|
const wordCount = mnemonic.split(/\s+/).length;
|
||||||
|
formatHint.textContent = `✅ Generated valid ${wordCount}-word mnemonic`;
|
||||||
|
formatHint.style.color = '#059669';
|
||||||
|
|
||||||
|
console.log('Generated new seed phrase:', wordCount, '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 title = document.createElement('h3');
|
||||||
|
title.textContent = 'Select Account';
|
||||||
|
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||||
|
|
||||||
|
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(title);
|
||||||
|
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: left; border: 1px solid #d1d5db; font-weight: bold;">Public Key (npub)</th>
|
||||||
|
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Action</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 pubkeyCell = document.createElement('td');
|
||||||
|
pubkeyCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db; font-family: monospace; word-break: break-all;';
|
||||||
|
|
||||||
|
// Show truncated npub for readability
|
||||||
|
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||||
|
pubkeyCell.innerHTML = `
|
||||||
|
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 2px;">${truncatedNpub}</code><br>
|
||||||
|
<small style="color: #6b7280;">Full: ${account.npub}</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const actionCell = document.createElement('td');
|
||||||
|
actionCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db;';
|
||||||
|
|
||||||
|
const selectButton = document.createElement('button');
|
||||||
|
selectButton.textContent = 'Use';
|
||||||
|
selectButton.onclick = () => this._selectAccount(account);
|
||||||
|
selectButton.style.cssText = `
|
||||||
|
padding: 4px 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: var(--nl-font-family, 'Courier New', monospace);
|
||||||
|
`;
|
||||||
|
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(pubkeyCell);
|
||||||
|
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() {
|
_showOtpScreen() {
|
||||||
// Placeholder for OTP functionality
|
// Placeholder for OTP functionality
|
||||||
this._showError('OTP/DM not yet implemented - coming soon!');
|
this._showError('OTP/DM not yet implemented - coming soon!');
|
||||||
@@ -2503,7 +2317,7 @@ class FloatingTab {
|
|||||||
// Determine which relays to use
|
// Determine which relays to use
|
||||||
const relays = this.options.getUserRelay.length > 0
|
const relays = this.options.getUserRelay.length > 0
|
||||||
? this.options.getUserRelay
|
? 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);
|
console.log('FloatingTab: Fetching profile from relays:', relays);
|
||||||
|
|
||||||
@@ -2695,10 +2509,10 @@ class NostrLite {
|
|||||||
|
|
||||||
this.options = {
|
this.options = {
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
||||||
methods: {
|
methods: {
|
||||||
extension: true,
|
extension: true,
|
||||||
local: true,
|
local: true,
|
||||||
|
seedphrase: false,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
connect: false,
|
connect: false,
|
||||||
otp: false
|
otp: false
|
||||||
@@ -3127,8 +2941,8 @@ class WindowNostr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getRelays() {
|
async getRelays() {
|
||||||
// Return configured relays from nostr-lite options
|
// Return default relays since we removed the relays configuration
|
||||||
return this.nostrLite.options?.relays || ['wss://relay.damus.io'];
|
return ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||||
}
|
}
|
||||||
|
|
||||||
get nip04() {
|
get nip04() {
|
||||||
|
|||||||
5456
lite/nostr.bundle.js
5456
lite/nostr.bundle.js
File diff suppressed because it is too large
Load Diff
321
lite/ui/modal.js
321
lite/ui/modal.js
@@ -58,7 +58,7 @@ class Modal {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Modal content: centered with margin
|
// Modal content: centered with margin, no fixed height
|
||||||
modalContent.style.cssText = `
|
modalContent.style.cssText = `
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--nl-secondary-color);
|
background: var(--nl-secondary-color);
|
||||||
@@ -68,7 +68,6 @@ class Modal {
|
|||||||
margin: 50px auto;
|
margin: 50px auto;
|
||||||
border-radius: var(--nl-border-radius, 15px);
|
border-radius: var(--nl-border-radius, 15px);
|
||||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||||
max-height: 600px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -133,8 +132,6 @@ class Modal {
|
|||||||
this.modalBody = document.createElement('div');
|
this.modalBody = document.createElement('div');
|
||||||
this.modalBody.style.cssText = `
|
this.modalBody.style.cssText = `
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 500px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
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)
|
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
|
||||||
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
||||||
options.push({
|
options.push({
|
||||||
@@ -280,6 +287,27 @@ 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]',
|
||||||
|
'🌱': '[SEED]',
|
||||||
|
'🌐': '[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;';
|
||||||
|
|
||||||
@@ -303,6 +331,7 @@ 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);
|
||||||
});
|
});
|
||||||
@@ -319,6 +348,9 @@ class Modal {
|
|||||||
case 'local':
|
case 'local':
|
||||||
this._showLocalKeyScreen();
|
this._showLocalKeyScreen();
|
||||||
break;
|
break;
|
||||||
|
case 'seedphrase':
|
||||||
|
this._showSeedPhraseScreen();
|
||||||
|
break;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
this._showConnectScreen();
|
this._showConnectScreen();
|
||||||
break;
|
break;
|
||||||
@@ -1363,6 +1395,287 @@ class Modal {
|
|||||||
this._setAuthMethod('readonly');
|
this._setAuthMethod('readonly');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_showSeedPhraseScreen() {
|
||||||
|
this.modalBody.innerHTML = '';
|
||||||
|
|
||||||
|
const title = document.createElement('h3');
|
||||||
|
title.textContent = 'Import from Seed Phrase';
|
||||||
|
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||||
|
|
||||||
|
const description = document.createElement('p');
|
||||||
|
description.textContent = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts:';
|
||||||
|
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;';
|
||||||
|
|
||||||
|
textarea.oninput = () => {
|
||||||
|
const value = textarea.value.trim();
|
||||||
|
if (!value) {
|
||||||
|
formatHint.textContent = '';
|
||||||
|
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';
|
||||||
|
} else {
|
||||||
|
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
|
||||||
|
formatHint.style.color = '#dc2626';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate new seed phrase button
|
||||||
|
const generateButton = document.createElement('button');
|
||||||
|
generateButton.textContent = 'Generate New Seed Phrase';
|
||||||
|
generateButton.onclick = () => this._generateNewSeedPhrase(textarea, formatHint);
|
||||||
|
generateButton.style.cssText = this._getButtonStyle() + 'margin-bottom: 12px;';
|
||||||
|
|
||||||
|
const importButton = document.createElement('button');
|
||||||
|
importButton.textContent = 'Import Accounts';
|
||||||
|
importButton.onclick = () => this._importFromSeedPhrase(textarea.value);
|
||||||
|
importButton.style.cssText = this._getButtonStyle();
|
||||||
|
|
||||||
|
const backButton = document.createElement('button');
|
||||||
|
backButton.textContent = 'Back';
|
||||||
|
backButton.onclick = () => this._renderLoginOptions();
|
||||||
|
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||||
|
|
||||||
|
this.modalBody.appendChild(title);
|
||||||
|
this.modalBody.appendChild(description);
|
||||||
|
this.modalBody.appendChild(textarea);
|
||||||
|
this.modalBody.appendChild(formatHint);
|
||||||
|
this.modalBody.appendChild(generateButton);
|
||||||
|
this.modalBody.appendChild(importButton);
|
||||||
|
this.modalBody.appendChild(backButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 validation to show it's valid
|
||||||
|
const wordCount = mnemonic.split(/\s+/).length;
|
||||||
|
formatHint.textContent = `✅ Generated valid ${wordCount}-word mnemonic`;
|
||||||
|
formatHint.style.color = '#059669';
|
||||||
|
|
||||||
|
console.log('Generated new seed phrase:', wordCount, '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 title = document.createElement('h3');
|
||||||
|
title.textContent = 'Select Account';
|
||||||
|
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||||
|
|
||||||
|
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(title);
|
||||||
|
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: left; border: 1px solid #d1d5db; font-weight: bold;">Public Key (npub)</th>
|
||||||
|
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Action</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 pubkeyCell = document.createElement('td');
|
||||||
|
pubkeyCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db; font-family: monospace; word-break: break-all;';
|
||||||
|
|
||||||
|
// Show truncated npub for readability
|
||||||
|
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||||
|
pubkeyCell.innerHTML = `
|
||||||
|
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 2px;">${truncatedNpub}</code><br>
|
||||||
|
<small style="color: #6b7280;">Full: ${account.npub}</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const actionCell = document.createElement('td');
|
||||||
|
actionCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db;';
|
||||||
|
|
||||||
|
const selectButton = document.createElement('button');
|
||||||
|
selectButton.textContent = 'Use';
|
||||||
|
selectButton.onclick = () => this._selectAccount(account);
|
||||||
|
selectButton.style.cssText = `
|
||||||
|
padding: 4px 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: var(--nl-font-family, 'Courier New', monospace);
|
||||||
|
`;
|
||||||
|
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(pubkeyCell);
|
||||||
|
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() {
|
_showOtpScreen() {
|
||||||
// Placeholder for OTP functionality
|
// Placeholder for OTP functionality
|
||||||
this._showError('OTP/DM not yet implemented - coming soon!');
|
this._showError('OTP/DM not yet implemented - coming soon!');
|
||||||
|
|||||||
Reference in New Issue
Block a user