1039 lines
30 KiB
JavaScript
1039 lines
30 KiB
JavaScript
/**
|
|
* NOSTR_LOGIN_LITE
|
|
* A minimal, dependency-light replacement for the current auth/UI stack
|
|
* Preserves all login methods and window.nostr surface
|
|
*/
|
|
|
|
// Import NIP-46 client
|
|
if (typeof NIP46Client === 'undefined') {
|
|
// Load NIP46Client if not already available (for non-bundled version)
|
|
const script = document.createElement('script');
|
|
script.src = './core/nip46-client.js';
|
|
document.head.appendChild(script);
|
|
}
|
|
|
|
// Global state
|
|
const LiteState = {
|
|
initialized: false,
|
|
windowNostr: null,
|
|
options: null,
|
|
auth: null,
|
|
modal: null,
|
|
bus: null,
|
|
pool: null,
|
|
nip44Codec: null,
|
|
extensionBridge: null,
|
|
nip46Client: null
|
|
};
|
|
|
|
// Dependencies verification
|
|
class Deps {
|
|
static ensureNostrToolsLoaded() {
|
|
if (typeof window === 'undefined') {
|
|
throw new Error('NOSTR_LOGIN_LITE must run in browser environment');
|
|
}
|
|
|
|
if (!window.NostrTools) {
|
|
throw new Error(
|
|
'window.NostrTools is required but not loaded. ' +
|
|
'Please include: <script src="./lite/nostr.bundle.js"></script>'
|
|
);
|
|
}
|
|
|
|
// Verify required APIs
|
|
const required = ['SimplePool', 'getPublicKey', 'finalizeEvent', 'nip04'];
|
|
for (const api of required) {
|
|
if (!window.NostrTools[api]) {
|
|
throw new Error(`window.NostrTools.${api} is required but missing`);
|
|
}
|
|
}
|
|
|
|
// Check for key generation function (might be generateSecretKey or generatePrivateKey)
|
|
if (!window.NostrTools.generateSecretKey && !window.NostrTools.generatePrivateKey) {
|
|
throw new Error('window.NostrTools must have either generateSecretKey or generatePrivateKey');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Event Bus for internal communication
|
|
class Bus {
|
|
constructor() {
|
|
this.handlers = {};
|
|
}
|
|
|
|
on(event, handler) {
|
|
if (!this.handlers[event]) {
|
|
this.handlers[event] = [];
|
|
}
|
|
this.handlers[event].push(handler);
|
|
}
|
|
|
|
off(event, handler) {
|
|
if (!this.handlers[event]) return;
|
|
this.handlers[event] = this.handlers[event].filter(h => h !== handler);
|
|
}
|
|
|
|
emit(event, payload) {
|
|
if (!this.handlers[event]) return;
|
|
this.handlers[event].forEach(handler => {
|
|
try {
|
|
handler(payload);
|
|
} catch (e) {
|
|
console.error(`Error in event handler for ${event}:`, e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Storage helpers
|
|
class Store {
|
|
static addAccount(info) {
|
|
const accounts = this.getAccounts();
|
|
// Remove existing account with same pubkey if present
|
|
const filtered = accounts.filter(acc => acc.pubkey !== info.pubkey);
|
|
filtered.push(info);
|
|
localStorage.setItem('nl_accounts', JSON.stringify(filtered));
|
|
}
|
|
|
|
static removeCurrentAccount() {
|
|
const current = this.getCurrent();
|
|
if (current && current.pubkey) {
|
|
const accounts = this.getAccounts();
|
|
const filtered = accounts.filter(acc => acc.pubkey !== current.pubkey);
|
|
localStorage.setItem('nl_accounts', JSON.stringify(filtered));
|
|
localStorage.removeItem('nl_current');
|
|
}
|
|
}
|
|
|
|
static getCurrent() {
|
|
try {
|
|
const stored = localStorage.getItem('nl_current');
|
|
return stored ? JSON.parse(stored) : null;
|
|
} catch (e) {
|
|
console.error('Error parsing current account:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static setCurrent(info) {
|
|
localStorage.setItem('nl_current', JSON.stringify(info));
|
|
}
|
|
|
|
static getAccounts() {
|
|
try {
|
|
const stored = localStorage.getItem('nl_accounts');
|
|
return stored ? JSON.parse(stored) : [];
|
|
} catch (e) {
|
|
console.error('Error parsing accounts:', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static getRecents() {
|
|
// Return last 5 used accounts in reverse chronological order
|
|
const accounts = this.getAccounts().slice(-5).reverse();
|
|
return accounts;
|
|
}
|
|
|
|
static setItem(key, value) {
|
|
localStorage.setItem(`nl-${key}`, value);
|
|
}
|
|
|
|
static getItem(key) {
|
|
return localStorage.getItem(`nl-${key}`);
|
|
}
|
|
|
|
static async getIcon() {
|
|
// Simple default icon - could be extended to fetch from profile
|
|
return '🔑';
|
|
}
|
|
}
|
|
|
|
// Relay configuration helpers
|
|
class Relays {
|
|
static getDefaultRelays(options) {
|
|
if (options?.relays) {
|
|
return this.normalize(options.relays);
|
|
}
|
|
|
|
// Default relays for fallbacks
|
|
return [
|
|
'wss://relay.damus.io',
|
|
'wss://relay.snort.social',
|
|
'wss://nos.lol'
|
|
];
|
|
}
|
|
|
|
static normalize(relays) {
|
|
return relays.map(relay => {
|
|
// Ensure wss:// prefix
|
|
if (relay.startsWith('ws://')) {
|
|
return relay.replace('ws://', 'wss://');
|
|
} else if (!relay.startsWith('wss://')) {
|
|
return `wss://${relay}`;
|
|
}
|
|
return relay;
|
|
}).filter(relay => {
|
|
// Remove duplicates and validate URLs
|
|
try {
|
|
new URL(relay);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}).filter((relay, index, self) => self.indexOf(relay) === index); // dedupe
|
|
}
|
|
}
|
|
|
|
// Minimal NIP-44 codec fallback
|
|
class Nip44 {
|
|
constructor() {
|
|
this.Nip44 = null;
|
|
// Initialize with existing codec if available
|
|
this.nip44Available = window.NostrTools?.nip44;
|
|
}
|
|
|
|
static encrypt(ourSk, theirPk, plaintext) {
|
|
if (window.NostrTools?.nip44?.encrypt) {
|
|
return window.NostrTools.nip44.encrypt(ourSk, theirPk, plaintext);
|
|
}
|
|
|
|
throw new Error('NIP-44 encryption not available. Please use nostr-tools@>=2.x or provide codec implementation.');
|
|
}
|
|
|
|
static decrypt(ourSk, theirPk, ciphertext) {
|
|
if (window.NostrTools?.nip44?.decrypt) {
|
|
return window.NostrTools.nip44.decrypt(ourSk, theirPk, ciphertext);
|
|
}
|
|
|
|
throw new Error('NIP-44 decryption not available. Please use nostr-tools@>=2.x or provide codec implementation.');
|
|
}
|
|
}
|
|
|
|
// LocalSigner wrapping window.NostrTools
|
|
class LocalSigner {
|
|
constructor(sk) {
|
|
this.sk = sk;
|
|
// Generate pubkey from secret key
|
|
this.pk = this._getPubKey();
|
|
}
|
|
|
|
_getPubKey() {
|
|
const seckey = this.sk.startsWith('nsec') ?
|
|
window.NostrTools.nip19.decode(this.sk).data :
|
|
this.sk;
|
|
return window.NostrTools.getPublicKey(seckey);
|
|
}
|
|
|
|
pubkey() {
|
|
return this.pk;
|
|
}
|
|
|
|
async sign(event) {
|
|
// Prepare event for signing
|
|
const ev = { ...event };
|
|
ev.pubkey = this.pk;
|
|
|
|
// Generate event ID and sign
|
|
const signedEvent = await window.NostrTools.finalizeEvent(ev, this.sk);
|
|
return signedEvent;
|
|
}
|
|
|
|
async encrypt04(pubkey, plaintext) {
|
|
return await window.NostrTools.nip04.encrypt(this.sk, pubkey, plaintext);
|
|
}
|
|
|
|
async decrypt04(pubkey, ciphertext) {
|
|
return await window.NostrTools.nip04.decrypt(this.sk, pubkey, ciphertext);
|
|
}
|
|
|
|
async encrypt44(pubkey, plaintext) {
|
|
return Nip44.encrypt(this.sk, pubkey, plaintext);
|
|
}
|
|
|
|
async decrypt44(pubkey, ciphertext) {
|
|
return Nip44.decrypt(this.sk, pubkey, ciphertext);
|
|
}
|
|
}
|
|
|
|
// ExtensionBridge for detecting and managing browser extensions
|
|
class ExtensionBridge {
|
|
constructor() {
|
|
this.checking = false;
|
|
this.checkInterval = null;
|
|
this.originalNostr = null;
|
|
this.foundExtensions = new Map(); // Store multiple extensions by location
|
|
this.primaryExtension = null; // The currently selected extension
|
|
}
|
|
|
|
startChecking(nostrLite) {
|
|
if (this.checking) return;
|
|
this.checking = true;
|
|
|
|
const check = () => {
|
|
this.detectAllExtensions(nostrLite);
|
|
};
|
|
|
|
// Check immediately
|
|
check();
|
|
|
|
// Then check every 200ms for 30 seconds
|
|
this.checkInterval = setInterval(check, 200);
|
|
|
|
// Stop checking after 30 seconds
|
|
setTimeout(() => {
|
|
clearInterval(this.checkInterval);
|
|
this.checkInterval = null;
|
|
}, 30000);
|
|
}
|
|
|
|
detectAllExtensions(nostrLite) {
|
|
// Extension locations to check (in priority order)
|
|
const locations = [
|
|
{ path: 'window.navigator?.nostr', name: 'navigator.nostr', getter: () => window.navigator?.nostr },
|
|
{ path: 'window.webln?.nostr', name: 'webln.nostr', getter: () => window.webln?.nostr },
|
|
{ path: 'window.alby?.nostr', name: 'alby.nostr', getter: () => window.alby?.nostr },
|
|
{ path: 'window.nos2x', name: 'nos2x', getter: () => window.nos2x },
|
|
{ path: 'window.flamingo?.nostr', name: 'flamingo.nostr', getter: () => window.flamingo?.nostr },
|
|
{ path: 'window.mutiny?.nostr', name: 'mutiny.nostr', getter: () => window.mutiny?.nostr },
|
|
{ path: 'window.nostrich?.nostr', name: 'nostrich.nostr', getter: () => window.nostrich?.nostr },
|
|
{ path: 'window.getAlby?.nostr', name: 'getAlby.nostr', getter: () => window.getAlby?.nostr }
|
|
];
|
|
|
|
let foundNew = false;
|
|
|
|
// Check each location
|
|
for (const location of locations) {
|
|
try {
|
|
const obj = location.getter();
|
|
|
|
if (obj && this.isRealExtension(obj, nostrLite)) {
|
|
if (!this.foundExtensions.has(location.name)) {
|
|
this.foundExtensions.set(location.name, {
|
|
name: location.name,
|
|
path: location.path,
|
|
extension: obj,
|
|
constructor: obj.constructor?.name || 'Unknown'
|
|
});
|
|
console.log(`Real Nostr extension detected: ${location.name} (${obj.constructor?.name})`);
|
|
foundNew = true;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Location doesn't exist or can't be accessed
|
|
}
|
|
}
|
|
|
|
// Also check window.nostr but be extra careful to avoid our library
|
|
if (window.nostr && this.isRealExtension(window.nostr, nostrLite)) {
|
|
// Make sure we haven't already detected this extension via another path
|
|
const existingExtension = Array.from(this.foundExtensions.values()).find(
|
|
ext => ext.extension === window.nostr
|
|
);
|
|
|
|
if (!existingExtension && !this.foundExtensions.has('window.nostr')) {
|
|
this.foundExtensions.set('window.nostr', {
|
|
name: 'window.nostr',
|
|
path: 'window.nostr',
|
|
extension: window.nostr,
|
|
constructor: window.nostr.constructor?.name || 'Unknown'
|
|
});
|
|
console.log(`Real Nostr extension detected at window.nostr: ${window.nostr.constructor?.name}`);
|
|
foundNew = true;
|
|
}
|
|
}
|
|
|
|
// Set primary extension if we don't have one and found extensions
|
|
if (!this.primaryExtension && this.foundExtensions.size > 0) {
|
|
// Prefer navigator.nostr if available, otherwise use first found
|
|
this.primaryExtension = this.foundExtensions.get('navigator.nostr') ||
|
|
Array.from(this.foundExtensions.values())[0];
|
|
|
|
// Cache the extension and reassign window.nostr to our lite version
|
|
this.originalNostr = this.primaryExtension.extension;
|
|
if (window.nostr !== nostrLite) {
|
|
window.nostr = nostrLite;
|
|
}
|
|
|
|
console.log(`Primary extension set: ${this.primaryExtension.name}`);
|
|
|
|
// If currently authenticated, reconcile state
|
|
if (LiteState.auth?.signer?.method === 'extension') {
|
|
this.reconcileExtension();
|
|
}
|
|
}
|
|
}
|
|
|
|
isRealExtension(obj, nostrLite) {
|
|
if (!obj || typeof obj !== 'object') return false;
|
|
|
|
// Must have required Nostr methods
|
|
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
|
return false;
|
|
}
|
|
|
|
// Exclude our own library objects
|
|
if (obj === nostrLite || obj === windowNostr) {
|
|
return false;
|
|
}
|
|
|
|
// Exclude objects with our library's internal methods
|
|
if (typeof obj._hexToUint8Array === 'function' || typeof obj._call === 'function') {
|
|
return false;
|
|
}
|
|
|
|
// Exclude NostrTools library object
|
|
if (obj === window.NostrTools) {
|
|
return false;
|
|
}
|
|
|
|
// Real extensions typically have proper constructors (not plain Object)
|
|
const constructorName = obj.constructor?.name;
|
|
if (constructorName === 'Object' && !obj._isEnabled && !obj.enabled) {
|
|
// Plain objects without extension-specific properties are likely our library
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
getAllExtensions() {
|
|
return Array.from(this.foundExtensions.values());
|
|
}
|
|
|
|
getExtensionCount() {
|
|
return this.foundExtensions.size;
|
|
}
|
|
|
|
hasExtension() {
|
|
return this.foundExtensions.size > 0;
|
|
}
|
|
|
|
// Legacy compatibility - return primary extension
|
|
get foundExtension() {
|
|
return this.primaryExtension?.extension || null;
|
|
}
|
|
|
|
// Method to properly set primary extension
|
|
setPrimaryExtension(extension, name = 'selected') {
|
|
// Find the extension in our map or create new entry
|
|
let extensionInfo = null;
|
|
|
|
// Check if this extension is already in our map
|
|
for (const [key, info] of this.foundExtensions) {
|
|
if (info.extension === extension) {
|
|
extensionInfo = info;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If not found, create a new entry
|
|
if (!extensionInfo) {
|
|
extensionInfo = {
|
|
name: name,
|
|
path: name,
|
|
extension: extension,
|
|
constructor: extension?.constructor?.name || 'Unknown'
|
|
};
|
|
this.foundExtensions.set(name, extensionInfo);
|
|
}
|
|
|
|
this.primaryExtension = extensionInfo;
|
|
console.log(`Primary extension set to: ${extensionInfo.name}`);
|
|
}
|
|
|
|
async setExtensionReadPubkey(expectedPubkey = null) {
|
|
if (!this.primaryExtension) return false;
|
|
|
|
try {
|
|
// Temporarily set window.nostr to extension
|
|
const temp = window.nostr;
|
|
window.nostr = this.primaryExtension.extension;
|
|
|
|
const pubkey = await this.primaryExtension.extension.getPublicKey();
|
|
|
|
// Restore our lite implementation
|
|
window.nostr = temp;
|
|
|
|
if (expectedPubkey && pubkey !== expectedPubkey) {
|
|
console.warn(`Extension pubkey ${pubkey} does not match expected ${expectedPubkey}`);
|
|
}
|
|
|
|
return pubkey;
|
|
} catch (e) {
|
|
console.error('Error reading extension pubkey:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
trySetForPubkey(expectedPubkey) {
|
|
if (!this.hasExtension()) return false;
|
|
|
|
this.setExtensionReadPubkey(expectedPubkey).then(pubkey => {
|
|
if (pubkey) {
|
|
LiteState.bus?.emit('extensionLogin', { pubkey });
|
|
}
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
setExtension() {
|
|
if (!this.primaryExtension) return;
|
|
window.nostr = this.primaryExtension.extension;
|
|
this.setExtensionReadPubkey().then(pubkey => {
|
|
if (pubkey) {
|
|
LiteState.bus?.emit('extensionSet', { pubkey });
|
|
}
|
|
});
|
|
}
|
|
|
|
unset(nostrLite) {
|
|
window.nostr = nostrLite;
|
|
}
|
|
|
|
reconcileExtension() {
|
|
// Handle extension state changes
|
|
this.setExtensionReadPubkey().then(pubkey => {
|
|
if (pubkey) {
|
|
// Update current account if extension is the signer
|
|
const current = Store.getCurrent();
|
|
if (current && current.signer?.method === 'extension') {
|
|
const info = {
|
|
...current,
|
|
pubkey,
|
|
signer: { method: 'extension' }
|
|
};
|
|
Store.setCurrent(info);
|
|
LiteState.bus?.emit('authStateUpdate', info);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Main API surface
|
|
class NostrLite {
|
|
static async init(options = {}) {
|
|
// Ensure dependencies are loaded
|
|
Deps.ensureNostrToolsLoaded();
|
|
|
|
// Prevent double initialization
|
|
if (LiteState.initialized) {
|
|
console.warn('NOSTR_LOGIN_LITE already initialized');
|
|
return;
|
|
}
|
|
|
|
// Initialize components
|
|
LiteState.bus = new Bus();
|
|
LiteState.extensionBridge = new ExtensionBridge();
|
|
|
|
// Initialize NIP-46 client
|
|
LiteState.nip46Client = new NIP46Client();
|
|
|
|
// Store options
|
|
LiteState.options = {
|
|
theme: 'light',
|
|
darkMode: false,
|
|
relays: Relays.getDefaultRelays(options),
|
|
methods: {
|
|
connect: true,
|
|
extension: true,
|
|
local: true,
|
|
readonly: true,
|
|
otp: true
|
|
},
|
|
otp: {},
|
|
...options
|
|
};
|
|
|
|
// Start extension detection
|
|
LiteState.extensionBridge.startChecking(windowNostr);
|
|
|
|
// Setup auth methods
|
|
this._setupAuth();
|
|
|
|
// Initialize modal UI
|
|
// this._initModal();
|
|
|
|
console.log('NOSTR_LOGIN_LITE initialized with options:', LiteState.options);
|
|
LiteState.initialized = true;
|
|
}
|
|
|
|
static _setupAuth() {
|
|
// Set up event listeners for modal interactions
|
|
window.addEventListener('nlMethodSelected', (event) => {
|
|
this._handleMethodSelected(event.detail);
|
|
});
|
|
|
|
// Set up other auth-related event listeners
|
|
this._setupAuthEventListeners();
|
|
|
|
console.log('Auth system setup loaded');
|
|
}
|
|
|
|
static _setupAuthEventListeners() {
|
|
// Handle extension detection
|
|
this.bus?.on('extensionDetected', (extension) => {
|
|
console.log('Extension detected');
|
|
LiteState.extensionBridge.setPrimaryExtension(extension, 'detected');
|
|
});
|
|
|
|
// Handle auth URL from NIP-46
|
|
window.addEventListener('nlAuthUrl', (event) => {
|
|
console.log('Auth URL received:', event.detail.url);
|
|
// Could show URL in modal or trigger external flow
|
|
});
|
|
|
|
// Handle logout events
|
|
window.addEventListener('nlLogout', () => {
|
|
console.log('Logout event received');
|
|
this.logout();
|
|
});
|
|
}
|
|
|
|
static _handleMethodSelected(detail) {
|
|
console.log('Method selected:', detail);
|
|
|
|
const { method, pubkey, secret, extension } = detail;
|
|
|
|
switch (method) {
|
|
case 'local':
|
|
if (secret && pubkey) {
|
|
// Set up local key authentication
|
|
const info = {
|
|
pubkey,
|
|
signer: { method: 'local', secret }
|
|
};
|
|
Store.setCurrent(info);
|
|
LiteState.bus?.emit('authStateUpdate', info);
|
|
this._dispatchAuthEvent('login', info);
|
|
}
|
|
break;
|
|
|
|
case 'extension':
|
|
if (pubkey && extension) {
|
|
// Store the extension object in the ExtensionBridge for future use
|
|
LiteState.extensionBridge.setPrimaryExtension(extension, 'modal-selected');
|
|
LiteState.extensionBridge.originalNostr = extension;
|
|
|
|
// Set up extension authentication
|
|
const info = {
|
|
pubkey,
|
|
signer: { method: 'extension' }
|
|
};
|
|
Store.setCurrent(info);
|
|
LiteState.bus?.emit('authStateUpdate', info);
|
|
this._dispatchAuthEvent('login', info);
|
|
|
|
console.log('Extension authentication set up successfully');
|
|
} else {
|
|
// Fallback to extension bridge detection
|
|
LiteState.bus?.emit('authMethodSelected', { method: 'extension' });
|
|
}
|
|
break;
|
|
|
|
case 'readonly':
|
|
// Set read-only mode
|
|
const readonlyInfo = {
|
|
pubkey: '',
|
|
signer: { method: 'readonly' }
|
|
};
|
|
Store.setCurrent(readonlyInfo);
|
|
LiteState.bus?.emit('authStateUpdate', readonlyInfo);
|
|
this._dispatchAuthEvent('login', readonlyInfo);
|
|
break;
|
|
|
|
case 'nip46':
|
|
if (secret && pubkey) {
|
|
// Set up NIP-46 remote signing
|
|
const info = {
|
|
pubkey,
|
|
signer: { method: 'nip46', ...secret }
|
|
};
|
|
Store.setCurrent(info);
|
|
LiteState.bus?.emit('authStateUpdate', info);
|
|
this._dispatchAuthEvent('login', info);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
console.warn('Unhandled auth method:', method);
|
|
}
|
|
}
|
|
|
|
static _dispatchAuthEvent(type, info) {
|
|
const eventPayload = {
|
|
type,
|
|
info,
|
|
pubkey: info?.pubkey || '',
|
|
method: info?.signer?.method || '',
|
|
...info
|
|
};
|
|
|
|
// Dispatch the event
|
|
window.dispatchEvent(new CustomEvent('nlAuth', { detail: eventPayload }));
|
|
|
|
this.bus?.emit('nlAuth', eventPayload);
|
|
}
|
|
|
|
static launch(startScreen) {
|
|
if (!LiteState.initialized) {
|
|
throw new Error('NOSTR_LOGIN_LITE not initialized. Call init() first.');
|
|
}
|
|
|
|
console.log('Launch requested with screen:', startScreen);
|
|
|
|
// Initialize modal if needed
|
|
if (!LiteState.modal) {
|
|
// Import modal lazily
|
|
if (typeof Modal !== 'undefined') {
|
|
LiteState.modal = Modal.init(LiteState.options);
|
|
} else {
|
|
console.error('Modal component not available');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Open modal with specified screen
|
|
LiteState.modal.open({ startScreen });
|
|
}
|
|
|
|
static logout() {
|
|
if (!LiteState.initialized) return;
|
|
|
|
// Clear current account and state
|
|
Store.removeCurrentAccount();
|
|
|
|
// Reset internal state
|
|
LiteState.auth = null;
|
|
|
|
// Emit logout event
|
|
window.dispatchEvent(new CustomEvent('nlLogout'));
|
|
LiteState.bus?.emit('logout');
|
|
|
|
console.log('Logged out');
|
|
}
|
|
|
|
static setDarkMode(dark) {
|
|
if (!LiteState.options) return;
|
|
|
|
LiteState.options.darkMode = dark;
|
|
Store.setItem('darkMode', dark.toString());
|
|
|
|
// Update modal theme if initialized
|
|
if (LiteState.modal) {
|
|
// LiteState.modal.updateTheme();
|
|
}
|
|
|
|
window.dispatchEvent(new CustomEvent('nlDarkMode', { detail: { dark } }));
|
|
}
|
|
|
|
static setAuth(o) {
|
|
if (!o || !o.type) return;
|
|
|
|
console.log('setAuth called:', o);
|
|
|
|
// Validate request
|
|
if (!['login', 'signup', 'logout'].includes(o.type)) {
|
|
throw new Error(`Invalid auth type: ${o.type}`);
|
|
}
|
|
|
|
if (['login', 'signup'].includes(o.type) && !['connect', 'extension', 'local', 'otp', 'readOnly'].includes(o.method)) {
|
|
throw new Error(`Invalid auth method: ${o.method}`);
|
|
}
|
|
|
|
// Handle based on type
|
|
switch (o.type) {
|
|
case 'logout':
|
|
this.logout();
|
|
break;
|
|
default:
|
|
// Delegate to auth system - will be implemented
|
|
console.log('Auth delegation not yet implemented');
|
|
}
|
|
}
|
|
|
|
static cancelNeedAuth() {
|
|
// Cancel any ongoing auth flows
|
|
LiteState.bus?.emit('cancelAuth');
|
|
console.log('Auth flow cancelled');
|
|
}
|
|
}
|
|
|
|
// Initialize the window.nostr facade
|
|
const windowNostr = {
|
|
async getPublicKey() {
|
|
if (!LiteState.initialized) {
|
|
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
|
}
|
|
|
|
const current = Store.getCurrent();
|
|
if (current && current.pubkey) {
|
|
return current.pubkey;
|
|
}
|
|
|
|
// Trigger auth flow
|
|
const authPromise = new Promise((resolve, reject) => {
|
|
const handleAuth = (event) => {
|
|
window.removeEventListener('nlAuth', handleAuth);
|
|
if (event.detail.type === 'login' && event.detail.pubkey) {
|
|
resolve(event.detail.pubkey);
|
|
} else {
|
|
reject(new Error('Authentication cancelled'));
|
|
}
|
|
};
|
|
|
|
window.addEventListener('nlAuth', handleAuth);
|
|
|
|
// Set timeout
|
|
setTimeout(() => {
|
|
window.removeEventListener('nlAuth', handleAuth);
|
|
reject(new Error('Authentication timeout'));
|
|
}, 300000); // 5 minutes
|
|
});
|
|
|
|
// Launch auth modal
|
|
NostrLite.launch('login');
|
|
|
|
return authPromise;
|
|
},
|
|
|
|
async signEvent(event) {
|
|
if (!LiteState.initialized) {
|
|
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
|
}
|
|
|
|
let current = Store.getCurrent();
|
|
|
|
// If no current account, trigger auth
|
|
if (!current) {
|
|
await window.nostr.getPublicKey(); // This will trigger auth
|
|
current = Store.getCurrent();
|
|
if (!current) {
|
|
throw new Error('Authentication failed');
|
|
}
|
|
}
|
|
|
|
// Route to appropriate signer
|
|
if (current.signer?.method === 'local' && current.signer.secret) {
|
|
const signer = new LocalSigner(this._hexToUint8Array(current.signer.secret));
|
|
return await signer.sign(event);
|
|
} else if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
|
// Route to NIP-46 remote signer
|
|
try {
|
|
const bunkerSigner = current.signer.bunkerSigner;
|
|
const signedEvent = await bunkerSigner.signEvent(event);
|
|
return signedEvent;
|
|
} catch (error) {
|
|
console.error('NIP-46 signEvent failed:', error);
|
|
throw new Error(`NIP-46 signing failed: ${error.message}`);
|
|
}
|
|
} else if (current.signer?.method === 'readonly') {
|
|
throw new Error('Cannot sign events in read-only mode');
|
|
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
|
// Route to extension
|
|
const temp = window.nostr;
|
|
window.nostr = LiteState.extensionBridge.foundExtension;
|
|
try {
|
|
const signedEvent = await window.nostr.signEvent(event);
|
|
return signedEvent;
|
|
} finally {
|
|
window.nostr = temp;
|
|
}
|
|
}
|
|
|
|
throw new Error('No suitable signer available for current account');
|
|
},
|
|
|
|
_hexToUint8Array(hex) {
|
|
// Convert hex string to Uint8Array
|
|
const bytes = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
}
|
|
return bytes;
|
|
},
|
|
|
|
nip04: {
|
|
async encrypt(pubkey, plaintext) {
|
|
if (!LiteState.initialized) {
|
|
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
|
}
|
|
|
|
const current = Store.getCurrent();
|
|
if (!current) {
|
|
throw new Error('No authenticated user');
|
|
}
|
|
|
|
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
|
// Route to NIP-46 remote signer
|
|
try {
|
|
const bunkerSigner = current.signer.bunkerSigner;
|
|
return await bunkerSigner.nip04Encrypt(pubkey, plaintext);
|
|
} catch (error) {
|
|
console.error('NIP-46 nip04 encrypt failed:', error);
|
|
throw new Error(`NIP-46 encrypting failed: ${error.message}`);
|
|
}
|
|
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
|
const signer = new LocalSigner(current.signer.secret);
|
|
return await signer.encrypt04(pubkey, plaintext);
|
|
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
|
const temp = window.nostr;
|
|
window.nostr = LiteState.extensionBridge.foundExtension;
|
|
try {
|
|
return await window.nostr.nip04.encrypt(pubkey, plaintext);
|
|
} finally {
|
|
window.nostr = temp;
|
|
}
|
|
}
|
|
|
|
throw new Error('No suitable signer available for NIP-04 encryption');
|
|
},
|
|
|
|
async decrypt(pubkey, ciphertext) {
|
|
if (!LiteState.initialized) {
|
|
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
|
}
|
|
|
|
const current = Store.getCurrent();
|
|
if (!current) {
|
|
throw new Error('No authenticated user');
|
|
}
|
|
|
|
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
|
// Route to NIP-46 remote signer
|
|
try {
|
|
const bunkerSigner = current.signer.bunkerSigner;
|
|
return await bunkerSigner.nip04Decrypt(pubkey, ciphertext);
|
|
} catch (error) {
|
|
console.error('NIP-46 nip04 decrypt failed:', error);
|
|
throw new Error(`NIP-46 decrypting failed: ${error.message}`);
|
|
}
|
|
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
|
const signer = new LocalSigner(current.signer.secret);
|
|
return await signer.decrypt04(pubkey, ciphertext);
|
|
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
|
const temp = window.nostr;
|
|
window.nostr = LiteState.extensionBridge.foundExtension;
|
|
try {
|
|
return await window.nostr.nip04.decrypt(pubkey, ciphertext);
|
|
} finally {
|
|
window.nostr = temp;
|
|
}
|
|
}
|
|
|
|
throw new Error('No suitable signer available for NIP-04 decryption');
|
|
}
|
|
},
|
|
|
|
nip44: {
|
|
async encrypt(pubkey, plaintext) {
|
|
if (!LiteState.initialized) {
|
|
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
|
}
|
|
|
|
const current = Store.getCurrent();
|
|
if (!current) {
|
|
throw new Error('No authenticated user');
|
|
}
|
|
|
|
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
|
// Route to NIP-46 remote signer
|
|
try {
|
|
const bunkerSigner = current.signer.bunkerSigner;
|
|
return await bunkerSigner.nip44Encrypt(pubkey, plaintext);
|
|
} catch (error) {
|
|
console.error('NIP-46 nip44 encrypt failed:', error);
|
|
throw new Error(`NIP-46 encrypting failed: ${error.message}`);
|
|
}
|
|
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
|
const signer = new LocalSigner(current.signer.secret);
|
|
return await signer.encrypt44(pubkey, plaintext);
|
|
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
|
// Use extension if it supports nip44
|
|
const temp = window.nostr;
|
|
window.nostr = LiteState.extensionBridge.foundExtension;
|
|
try {
|
|
if (window.nostr.nip44) {
|
|
return await window.nostr.nip44.encrypt(pubkey, plaintext);
|
|
} else {
|
|
throw new Error('Extension does not support NIP-44');
|
|
}
|
|
} finally {
|
|
window.nostr = temp;
|
|
}
|
|
}
|
|
|
|
throw new Error('No suitable signer available for NIP-44 encryption');
|
|
},
|
|
|
|
async decrypt(pubkey, ciphertext) {
|
|
if (!LiteState.initialized) {
|
|
throw new Error('NOSTR_LOGIN_LITE not initialized');
|
|
}
|
|
|
|
const current = Store.getCurrent();
|
|
if (!current) {
|
|
throw new Error('No authenticated user');
|
|
}
|
|
|
|
if (current.signer?.method === 'nip46' && current.signer.bunkerSigner) {
|
|
// Route to NIP-46 remote signer
|
|
try {
|
|
const bunkerSigner = current.signer.bunkerSigner;
|
|
return await bunkerSigner.nip44Decrypt(pubkey, ciphertext);
|
|
} catch (error) {
|
|
console.error('NIP-46 nip44 decrypt failed:', error);
|
|
throw new Error(`NIP-46 decrypting failed: ${error.message}`);
|
|
}
|
|
} else if (current.signer?.method === 'local' && current.signer.secret) {
|
|
const signer = new LocalSigner(current.signer.secret);
|
|
return await signer.decrypt44(pubkey, ciphertext);
|
|
} else if (current.signer?.method === 'extension' && LiteState.extensionBridge?.hasExtension()) {
|
|
const temp = window.nostr;
|
|
window.nostr = LiteState.extensionBridge.foundExtension;
|
|
try {
|
|
if (window.nostr.nip44) {
|
|
return await window.nostr.nip44.decrypt(pubkey, ciphertext);
|
|
} else {
|
|
throw new Error('Extension does not support NIP-44');
|
|
}
|
|
} finally {
|
|
window.nostr = temp;
|
|
}
|
|
}
|
|
|
|
throw new Error('No suitable signer available for NIP-44 decryption');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Export the API
|
|
window.NOSTR_LOGIN_LITE = {
|
|
init: NostrLite.init.bind(NostrLite),
|
|
launch: NostrLite.launch.bind(NostrLite),
|
|
logout: NostrLite.logout.bind(NostrLite),
|
|
setDarkMode: NostrLite.setDarkMode.bind(NostrLite),
|
|
setAuth: NostrLite.setAuth.bind(NostrLite),
|
|
cancelNeedAuth: NostrLite.cancelNeedAuth.bind(NostrLite),
|
|
// Expose internal components for debugging
|
|
get _extensionBridge() {
|
|
return LiteState.extensionBridge;
|
|
},
|
|
get _state() {
|
|
return LiteState;
|
|
}
|
|
};
|
|
|
|
// Set window.nostr facade properly (extensions will be handled by ExtensionBridge)
|
|
if (typeof window !== 'undefined') {
|
|
window.nostr = windowNostr;
|
|
|
|
// Ensure all methods are properly exposed
|
|
console.log('NOSTR_LOGIN_LITE: window.nostr facade installed with methods:', Object.keys(windowNostr));
|
|
}
|
|
|
|
console.log('NOSTR_LOGIN_LITE loaded - use window.NOSTR_LOGIN_LITE.init(options) to initialize'); |