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: 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,7 +453,7 @@ 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);
|
||||
|
||||
@@ -1144,10 +645,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
|
||||
@@ -1576,8 +1077,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() {
|
||||
|
||||
Reference in New Issue
Block a user