4356 lines
148 KiB
JavaScript
4356 lines
148 KiB
JavaScript
/**
|
||
* NOSTR_LOGIN_LITE - Authentication Library
|
||
*
|
||
* ⚠️ WARNING: THIS FILE IS AUTO-GENERATED - DO NOT EDIT MANUALLY!
|
||
* ⚠️ To make changes, edit lite/build.js and run: cd lite && node build.js
|
||
* ⚠️ Any manual edits to this file will be OVERWRITTEN when build.js runs!
|
||
*
|
||
* Two-file architecture:
|
||
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
||
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
|
||
* Generated on: 2025-09-20T19:25:01.143Z
|
||
*/
|
||
|
||
// Verify dependencies are loaded
|
||
if (typeof window !== 'undefined') {
|
||
if (!window.NostrTools) {
|
||
console.error('NOSTR_LOGIN_LITE: nostr.bundle.js must be loaded first');
|
||
throw new Error('Missing dependency: nostr.bundle.js');
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// ======================================
|
||
// NOSTR_LOGIN_LITE Components
|
||
// ======================================
|
||
|
||
// ======================================
|
||
// CSS-Only Theme System
|
||
// ======================================
|
||
|
||
const THEME_CSS = {
|
||
'default': `/**
|
||
* NOSTR_LOGIN_LITE - Default Monospace Theme
|
||
* Black/white/red color scheme with monospace typography
|
||
* Simplified 14-variable system (6 core + 8 floating tab)
|
||
*/
|
||
|
||
:root {
|
||
/* Core Variables (6) */
|
||
--nl-primary-color: #000000;
|
||
--nl-secondary-color: #ffffff;
|
||
--nl-accent-color: #ff0000;
|
||
--nl-muted-color: #CCCCCC;
|
||
--nl-font-family: "Courier New", Courier, monospace;
|
||
--nl-border-radius: 15px;
|
||
--nl-border-width: 3px;
|
||
|
||
/* Floating Tab Variables (8) */
|
||
--nl-tab-bg-logged-out: #ffffff;
|
||
--nl-tab-bg-logged-in: #ffffff;
|
||
--nl-tab-bg-opacity-logged-out: 0.9;
|
||
--nl-tab-bg-opacity-logged-in: 0.2;
|
||
--nl-tab-color-logged-out: #000000;
|
||
--nl-tab-color-logged-in: #ffffff;
|
||
--nl-tab-border-logged-out: #000000;
|
||
--nl-tab-border-logged-in: #ff0000;
|
||
--nl-tab-border-opacity-logged-out: 1.0;
|
||
--nl-tab-border-opacity-logged-in: 0.1;
|
||
}
|
||
|
||
/* Base component styles using simplified variables */
|
||
.nl-component {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
}
|
||
|
||
.nl-button {
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
font-family: var(--nl-font-family);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.nl-button:hover {
|
||
border-color: var(--nl-accent-color);
|
||
}
|
||
|
||
.nl-button:active {
|
||
background: var(--nl-accent-color);
|
||
color: var(--nl-secondary-color);
|
||
}
|
||
|
||
.nl-input {
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
font-family: var(--nl-font-family);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.nl-input:focus {
|
||
border-color: var(--nl-accent-color);
|
||
outline: none;
|
||
}
|
||
|
||
.nl-container {
|
||
background: var(--nl-secondary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
}
|
||
|
||
.nl-title, .nl-heading {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
margin: 0;
|
||
}
|
||
|
||
.nl-text {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
}
|
||
|
||
.nl-text--muted {
|
||
color: var(--nl-muted-color);
|
||
}
|
||
|
||
.nl-icon {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
}
|
||
|
||
/* Floating tab styles */
|
||
.nl-floating-tab {
|
||
font-family: var(--nl-font-family);
|
||
border-radius: var(--nl-border-radius);
|
||
border: var(--nl-border-width) solid;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.nl-floating-tab--logged-out {
|
||
background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out));
|
||
color: var(--nl-tab-color-logged-out);
|
||
border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out));
|
||
}
|
||
|
||
.nl-floating-tab--logged-in {
|
||
background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in));
|
||
color: var(--nl-tab-color-logged-in);
|
||
border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in));
|
||
}
|
||
|
||
.nl-transition {
|
||
transition: all 0.2s ease;
|
||
}`,
|
||
'dark': `/**
|
||
* NOSTR_LOGIN_LITE - Dark Monospace Theme
|
||
*/
|
||
|
||
:root {
|
||
/* Core Variables (6) */
|
||
--nl-primary-color: #white;
|
||
--nl-secondary-color: #black;
|
||
--nl-accent-color: #ff0000;
|
||
--nl-muted-color: #666666;
|
||
--nl-font-family: "Courier New", Courier, monospace;
|
||
--nl-border-radius: 15px;
|
||
--nl-border-width: 3px;
|
||
|
||
/* Floating Tab Variables (8) */
|
||
--nl-tab-bg-logged-out: #ffffff;
|
||
--nl-tab-bg-logged-in: #000000;
|
||
--nl-tab-bg-opacity-logged-out: 0.9;
|
||
--nl-tab-bg-opacity-logged-in: 0.8;
|
||
--nl-tab-color-logged-out: #000000;
|
||
--nl-tab-color-logged-in: #ffffff;
|
||
--nl-tab-border-logged-out: #000000;
|
||
--nl-tab-border-logged-in: #ff0000;
|
||
--nl-tab-border-opacity-logged-out: 1.0;
|
||
--nl-tab-border-opacity-logged-in: 0.9;
|
||
}
|
||
|
||
/* Base component styles using simplified variables */
|
||
.nl-component {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
}
|
||
|
||
.nl-button {
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
font-family: var(--nl-font-family);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.nl-button:hover {
|
||
border-color: var(--nl-accent-color);
|
||
}
|
||
|
||
.nl-button:active {
|
||
background: var(--nl-accent-color);
|
||
color: var(--nl-secondary-color);
|
||
}
|
||
|
||
.nl-input {
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
font-family: var(--nl-font-family);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.nl-input:focus {
|
||
border-color: var(--nl-accent-color);
|
||
outline: none;
|
||
}
|
||
|
||
.nl-container {
|
||
background: var(--nl-secondary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
}
|
||
|
||
.nl-title, .nl-heading {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
margin: 0;
|
||
}
|
||
|
||
.nl-text {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
}
|
||
|
||
.nl-text--muted {
|
||
color: var(--nl-muted-color);
|
||
}
|
||
|
||
.nl-icon {
|
||
font-family: var(--nl-font-family);
|
||
color: var(--nl-primary-color);
|
||
}
|
||
|
||
/* Floating tab styles */
|
||
.nl-floating-tab {
|
||
font-family: var(--nl-font-family);
|
||
border-radius: var(--nl-border-radius);
|
||
border: var(--nl-border-width) solid;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.nl-floating-tab--logged-out {
|
||
background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out));
|
||
color: var(--nl-tab-color-logged-out);
|
||
border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out));
|
||
}
|
||
|
||
.nl-floating-tab--logged-in {
|
||
background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in));
|
||
color: var(--nl-tab-color-logged-in);
|
||
border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in));
|
||
}
|
||
|
||
.nl-transition {
|
||
transition: all 0.2s ease;
|
||
}`
|
||
};
|
||
|
||
// Theme management functions
|
||
function injectThemeCSS(themeName = 'default') {
|
||
if (typeof document !== 'undefined') {
|
||
// Remove existing theme CSS
|
||
const existingStyle = document.getElementById('nl-theme-css');
|
||
if (existingStyle) {
|
||
existingStyle.remove();
|
||
}
|
||
|
||
// Inject selected theme CSS
|
||
const themeCss = THEME_CSS[themeName] || THEME_CSS['default'];
|
||
const style = document.createElement('style');
|
||
style.id = 'nl-theme-css';
|
||
style.textContent = themeCss;
|
||
document.head.appendChild(style);
|
||
console.log(`NOSTR_LOGIN_LITE: ${themeName} theme CSS injected`);
|
||
}
|
||
}
|
||
|
||
// Auto-inject default theme when DOM is ready
|
||
if (typeof document !== 'undefined') {
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => injectThemeCSS('default'));
|
||
} else {
|
||
injectThemeCSS('default');
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// Modal UI Component
|
||
// ======================================
|
||
|
||
|
||
class Modal {
|
||
constructor(options = {}) {
|
||
this.options = options;
|
||
this.container = null;
|
||
this.isVisible = false;
|
||
this.currentScreen = null;
|
||
this.isEmbedded = !!options.embedded;
|
||
this.embeddedContainer = options.embedded;
|
||
|
||
// Initialize modal container and styles
|
||
this._initModal();
|
||
}
|
||
|
||
_initModal() {
|
||
// Create modal container
|
||
this.container = document.createElement('div');
|
||
this.container.id = this.isEmbedded ? 'nl-modal-embedded' : 'nl-modal';
|
||
|
||
if (this.isEmbedded) {
|
||
// Embedded mode: inline positioning, no overlay
|
||
this.container.style.cssText = `
|
||
position: relative;
|
||
display: none;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
width: 100%;
|
||
`;
|
||
} else {
|
||
// Modal mode: fixed overlay
|
||
this.container.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.75);
|
||
display: none;
|
||
z-index: 10000;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
}
|
||
|
||
// Create modal content
|
||
const modalContent = document.createElement('div');
|
||
if (this.isEmbedded) {
|
||
// Embedded content: no centering margin, full width
|
||
modalContent.style.cssText = `
|
||
position: relative;
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
width: 100%;
|
||
border-radius: var(--nl-border-radius, 15px);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
overflow: hidden;
|
||
`;
|
||
} else {
|
||
// Modal content: centered with margin, no fixed height
|
||
modalContent.style.cssText = `
|
||
position: relative;
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
width: 90%;
|
||
max-width: 400px;
|
||
margin: 50px auto;
|
||
border-radius: var(--nl-border-radius, 15px);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
overflow: hidden;
|
||
`;
|
||
}
|
||
|
||
// Header
|
||
const modalHeader = document.createElement('div');
|
||
modalHeader.style.cssText = `
|
||
padding: 20px 24px 0 24px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: transparent;
|
||
border-bottom: none;
|
||
`;
|
||
|
||
const modalTitle = document.createElement('h2');
|
||
modalTitle.textContent = 'Nostr Login';
|
||
modalTitle.style.cssText = `
|
||
margin: 0;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: var(--nl-primary-color);
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
modalHeader.appendChild(modalTitle);
|
||
|
||
// Only add close button for non-embedded modals
|
||
// Embedded modals shouldn't have a close button because there's no way to reopen them
|
||
if (!this.isEmbedded) {
|
||
const closeButton = document.createElement('button');
|
||
closeButton.innerHTML = '×';
|
||
closeButton.onclick = () => this.close();
|
||
closeButton.style.cssText = `
|
||
background: var(--nl-secondary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: 4px;
|
||
font-size: 28px;
|
||
color: var(--nl-primary-color);
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
closeButton.onmouseover = () => {
|
||
closeButton.style.borderColor = 'var(--nl-accent-color)';
|
||
closeButton.style.background = 'var(--nl-secondary-color)';
|
||
};
|
||
closeButton.onmouseout = () => {
|
||
closeButton.style.borderColor = 'var(--nl-primary-color)';
|
||
closeButton.style.background = 'var(--nl-secondary-color)';
|
||
};
|
||
|
||
modalHeader.appendChild(closeButton);
|
||
}
|
||
|
||
// Body
|
||
this.modalBody = document.createElement('div');
|
||
this.modalBody.style.cssText = `
|
||
padding: 24px;
|
||
background: transparent;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
modalContent.appendChild(modalHeader);
|
||
modalContent.appendChild(this.modalBody);
|
||
this.container.appendChild(modalContent);
|
||
|
||
// Add to appropriate parent
|
||
if (this.isEmbedded && this.embeddedContainer) {
|
||
// Append to specified container for embedding
|
||
if (typeof this.embeddedContainer === 'string') {
|
||
const targetElement = document.querySelector(this.embeddedContainer);
|
||
if (targetElement) {
|
||
targetElement.appendChild(this.container);
|
||
} else {
|
||
console.error('NOSTR_LOGIN_LITE: Embedded container not found:', this.embeddedContainer);
|
||
document.body.appendChild(this.container);
|
||
}
|
||
} else if (this.embeddedContainer instanceof HTMLElement) {
|
||
this.embeddedContainer.appendChild(this.container);
|
||
} else {
|
||
console.error('NOSTR_LOGIN_LITE: Invalid embedded container');
|
||
document.body.appendChild(this.container);
|
||
}
|
||
} else {
|
||
// Add to body for modal mode
|
||
document.body.appendChild(this.container);
|
||
}
|
||
|
||
// Click outside to close (only for modal mode)
|
||
if (!this.isEmbedded) {
|
||
this.container.onclick = (e) => {
|
||
if (e.target === this.container) {
|
||
this.close();
|
||
}
|
||
};
|
||
}
|
||
|
||
// Update theme
|
||
this.updateTheme();
|
||
}
|
||
|
||
updateTheme() {
|
||
// The theme will automatically update through CSS custom properties
|
||
// No manual styling needed - the CSS variables handle everything
|
||
}
|
||
|
||
open(opts = {}) {
|
||
this.currentScreen = opts.startScreen;
|
||
this.isVisible = true;
|
||
this.container.style.display = 'block';
|
||
|
||
// Render login options
|
||
this._renderLoginOptions();
|
||
}
|
||
|
||
close() {
|
||
this.isVisible = false;
|
||
this.container.style.display = 'none';
|
||
this.modalBody.innerHTML = '';
|
||
}
|
||
|
||
_renderLoginOptions() {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const options = [];
|
||
|
||
// Extension option
|
||
if (this.options?.methods?.extension !== false) {
|
||
options.push({
|
||
type: 'extension',
|
||
title: 'Browser Extension',
|
||
description: 'Use your browser extension',
|
||
icon: '🔌'
|
||
});
|
||
}
|
||
|
||
// Local key option
|
||
if (this.options?.methods?.local !== false) {
|
||
options.push({
|
||
type: 'local',
|
||
title: 'Local Key',
|
||
description: 'Create or import your own key',
|
||
icon: '🔑'
|
||
});
|
||
}
|
||
|
||
// Seed Phrase option - only show if explicitly enabled
|
||
if (this.options?.methods?.seedphrase === true) {
|
||
options.push({
|
||
type: 'seedphrase',
|
||
title: 'Seed Phrase',
|
||
description: 'Import from mnemonic seed phrase',
|
||
icon: '🌱'
|
||
});
|
||
}
|
||
|
||
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
|
||
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
||
options.push({
|
||
type: 'connect',
|
||
title: 'Nostr Connect',
|
||
description: 'Connect with external signer',
|
||
icon: '🌐'
|
||
});
|
||
}
|
||
|
||
// Read-only option
|
||
if (this.options?.methods?.readonly !== false) {
|
||
options.push({
|
||
type: 'readonly',
|
||
title: 'Read Only',
|
||
description: 'Browse without signing',
|
||
icon: '👁️'
|
||
});
|
||
}
|
||
|
||
// OTP/DM option
|
||
if (this.options?.methods?.otp !== false) {
|
||
options.push({
|
||
type: 'otp',
|
||
title: 'DM/OTP',
|
||
description: 'Receive OTP via DM',
|
||
icon: '📱'
|
||
});
|
||
}
|
||
|
||
// Render each option
|
||
options.forEach(option => {
|
||
const button = document.createElement('button');
|
||
button.onclick = () => this._handleOptionClick(option.type);
|
||
button.style.cssText = `
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
button.onmouseover = () => {
|
||
button.style.borderColor = 'var(--nl-accent-color)';
|
||
button.style.background = 'var(--nl-secondary-color)';
|
||
};
|
||
button.onmouseout = () => {
|
||
button.style.borderColor = 'var(--nl-primary-color)';
|
||
button.style.background = 'var(--nl-secondary-color)';
|
||
};
|
||
|
||
const iconDiv = document.createElement('div');
|
||
// Remove the icon entirely - no emojis or text-based icons
|
||
iconDiv.textContent = '';
|
||
iconDiv.style.cssText = `
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-right: 16px;
|
||
width: 0px;
|
||
text-align: center;
|
||
color: var(--nl-primary-color);
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.style.cssText = 'flex: 1; text-align: left;';
|
||
|
||
const titleDiv = document.createElement('div');
|
||
titleDiv.textContent = option.title;
|
||
titleDiv.style.cssText = `
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
color: var(--nl-primary-color);
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
const descDiv = document.createElement('div');
|
||
descDiv.textContent = option.description;
|
||
descDiv.style.cssText = `
|
||
font-size: 14px;
|
||
color: #666666;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
contentDiv.appendChild(titleDiv);
|
||
contentDiv.appendChild(descDiv);
|
||
|
||
button.appendChild(iconDiv);
|
||
button.appendChild(contentDiv);
|
||
this.modalBody.appendChild(button);
|
||
});
|
||
}
|
||
|
||
_handleOptionClick(type) {
|
||
console.log('Selected login type:', type);
|
||
|
||
// Handle different login types
|
||
switch (type) {
|
||
case 'extension':
|
||
this._handleExtension();
|
||
break;
|
||
case 'local':
|
||
this._showLocalKeyScreen();
|
||
break;
|
||
case 'seedphrase':
|
||
this._showSeedPhraseScreen();
|
||
break;
|
||
case 'connect':
|
||
this._showConnectScreen();
|
||
break;
|
||
case 'readonly':
|
||
this._handleReadonly();
|
||
break;
|
||
case 'otp':
|
||
this._showOtpScreen();
|
||
break;
|
||
}
|
||
}
|
||
|
||
_handleExtension() {
|
||
// SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension
|
||
let extension = null;
|
||
|
||
// Check if NostrLite instance has a preserved extension (real extension detected at init)
|
||
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||
console.log('Modal: Using preserved extension:', extension.constructor?.name);
|
||
}
|
||
// Otherwise check current window.nostr
|
||
else if (window.nostr && this._isRealExtension(window.nostr)) {
|
||
extension = window.nostr;
|
||
console.log('Modal: Using current window.nostr extension:', extension.constructor?.name);
|
||
}
|
||
|
||
if (!extension) {
|
||
console.log('Modal: No extension detected yet, waiting for deferred detection...');
|
||
|
||
// DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library
|
||
let attempts = 0;
|
||
const maxAttempts = 10; // Try for 2 seconds
|
||
const checkForExtension = () => {
|
||
attempts++;
|
||
|
||
// Check again for preserved extension (might be set by deferred detection)
|
||
if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) {
|
||
extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension;
|
||
console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name);
|
||
this._tryExtensionLogin(extension);
|
||
return;
|
||
}
|
||
|
||
// Check current window.nostr again
|
||
if (window.nostr && this._isRealExtension(window.nostr)) {
|
||
extension = window.nostr;
|
||
console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name);
|
||
this._tryExtensionLogin(extension);
|
||
return;
|
||
}
|
||
|
||
// Keep trying or give up
|
||
if (attempts < maxAttempts) {
|
||
setTimeout(checkForExtension, 200);
|
||
} else {
|
||
console.log('Modal: No browser extension found after waiting 2 seconds');
|
||
this._showExtensionRequired();
|
||
}
|
||
};
|
||
|
||
// Start checking after a brief delay
|
||
setTimeout(checkForExtension, 200);
|
||
return;
|
||
}
|
||
|
||
// Use the single detected extension directly - no choice UI
|
||
console.log('Modal: Single extension mode - using extension directly');
|
||
this._tryExtensionLogin(extension);
|
||
}
|
||
|
||
_detectAllExtensions() {
|
||
const extensions = [];
|
||
const seenExtensions = new Set(); // Track extensions by object reference to avoid duplicates
|
||
|
||
// Extension locations to check (in priority order)
|
||
const locations = [
|
||
{ path: 'window.navigator?.nostr', name: 'navigator.nostr', displayName: 'Standard Extension (navigator.nostr)', icon: '🌐', getter: () => window.navigator?.nostr },
|
||
{ path: 'window.webln?.nostr', name: 'webln.nostr', displayName: 'Alby WebLN Extension', icon: '⚡', getter: () => window.webln?.nostr },
|
||
{ path: 'window.alby?.nostr', name: 'alby.nostr', displayName: 'Alby Extension (Direct)', icon: '🐝', getter: () => window.alby?.nostr },
|
||
{ path: 'window.nos2x', name: 'nos2x', displayName: 'nos2x Extension', icon: '🔌', getter: () => window.nos2x },
|
||
{ path: 'window.flamingo?.nostr', name: 'flamingo.nostr', displayName: 'Flamingo Extension', icon: '🦩', getter: () => window.flamingo?.nostr },
|
||
{ path: 'window.mutiny?.nostr', name: 'mutiny.nostr', displayName: 'Mutiny Extension', icon: '⚔️', getter: () => window.mutiny?.nostr },
|
||
{ path: 'window.nostrich?.nostr', name: 'nostrich.nostr', displayName: 'Nostrich Extension', icon: '🐦', getter: () => window.nostrich?.nostr },
|
||
{ path: 'window.getAlby?.nostr', name: 'getAlby.nostr', displayName: 'getAlby Extension', icon: '🔧', getter: () => window.getAlby?.nostr }
|
||
];
|
||
|
||
// Check each location
|
||
for (const location of locations) {
|
||
try {
|
||
const obj = location.getter();
|
||
|
||
console.log(`Modal: Checking ${location.name}:`, !!obj, obj?.constructor?.name);
|
||
|
||
if (obj && this._isRealExtension(obj) && !seenExtensions.has(obj)) {
|
||
extensions.push({
|
||
name: location.name,
|
||
displayName: location.displayName,
|
||
icon: location.icon,
|
||
extension: obj
|
||
});
|
||
seenExtensions.add(obj);
|
||
console.log(`Modal: ✓ Detected extension at ${location.name} (${obj.constructor?.name})`);
|
||
} else if (obj) {
|
||
console.log(`Modal: ✗ Filtered out ${location.name} (${obj.constructor?.name})`);
|
||
}
|
||
} catch (e) {
|
||
// Location doesn't exist or can't be accessed
|
||
console.log(`Modal: ${location.name} not accessible:`, e.message);
|
||
}
|
||
}
|
||
|
||
// Also check window.nostr but be extra careful to avoid our library
|
||
console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name);
|
||
|
||
if (window.nostr) {
|
||
// Check if window.nostr is our WindowNostr facade with a preserved extension
|
||
if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) {
|
||
console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension');
|
||
const preservedExtension = window.nostr.existingNostr;
|
||
console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name);
|
||
|
||
if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) {
|
||
extensions.push({
|
||
name: 'window.nostr.existingNostr',
|
||
displayName: 'Extension (preserved by WindowNostr)',
|
||
icon: '🔑',
|
||
extension: preservedExtension
|
||
});
|
||
seenExtensions.add(preservedExtension);
|
||
console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`);
|
||
}
|
||
}
|
||
// Check if window.nostr is directly a real extension (not our facade)
|
||
else if (this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) {
|
||
extensions.push({
|
||
name: 'window.nostr',
|
||
displayName: 'Extension (window.nostr)',
|
||
icon: '🔑',
|
||
extension: window.nostr
|
||
});
|
||
seenExtensions.add(window.nostr);
|
||
console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`);
|
||
} else {
|
||
console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`);
|
||
}
|
||
}
|
||
|
||
return extensions;
|
||
}
|
||
|
||
_isRealExtension(obj) {
|
||
console.log(`Modal: EXTENSIVE DEBUG - _isRealExtension called with:`, obj);
|
||
console.log(`Modal: Object type: ${typeof obj}`);
|
||
console.log(`Modal: Object truthy: ${!!obj}`);
|
||
|
||
if (!obj || typeof obj !== 'object') {
|
||
console.log(`Modal: REJECT - Not an object`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`Modal: getPublicKey type: ${typeof obj.getPublicKey}`);
|
||
console.log(`Modal: signEvent type: ${typeof obj.signEvent}`);
|
||
|
||
// Must have required Nostr methods
|
||
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
||
console.log(`Modal: REJECT - Missing required methods`);
|
||
return false;
|
||
}
|
||
|
||
// Exclude NostrTools library object
|
||
if (obj === window.NostrTools) {
|
||
console.log(`Modal: REJECT - Is NostrTools object`);
|
||
return false;
|
||
}
|
||
|
||
// Use the EXACT SAME logic as the comprehensive test (lines 804-809)
|
||
// This is the key fix - match the comprehensive test's successful detection logic
|
||
const constructorName = obj.constructor?.name;
|
||
const objectKeys = Object.keys(obj);
|
||
|
||
console.log(`Modal: Constructor name: "${constructorName}"`);
|
||
console.log(`Modal: Object keys: [${objectKeys.join(', ')}]`);
|
||
|
||
// COMPREHENSIVE TEST LOGIC - Accept anything with required methods that's not our specific library classes
|
||
const isRealExtension = (
|
||
typeof obj.getPublicKey === 'function' &&
|
||
typeof obj.signEvent === 'function' &&
|
||
constructorName !== 'WindowNostr' && // Our library class
|
||
constructorName !== 'NostrLite' // Our main class
|
||
);
|
||
|
||
console.log(`Modal: Using comprehensive test logic:`);
|
||
console.log(` Has getPublicKey: ${typeof obj.getPublicKey === 'function'}`);
|
||
console.log(` Has signEvent: ${typeof obj.signEvent === 'function'}`);
|
||
console.log(` Not WindowNostr: ${constructorName !== 'WindowNostr'}`);
|
||
console.log(` Not NostrLite: ${constructorName !== 'NostrLite'}`);
|
||
console.log(` Constructor: "${constructorName}"`);
|
||
|
||
// Additional debugging for comparison
|
||
const extensionPropChecks = {
|
||
_isEnabled: !!obj._isEnabled,
|
||
enabled: !!obj.enabled,
|
||
kind: !!obj.kind,
|
||
_eventEmitter: !!obj._eventEmitter,
|
||
_scope: !!obj._scope,
|
||
_requests: !!obj._requests,
|
||
_pubkey: !!obj._pubkey,
|
||
name: !!obj.name,
|
||
version: !!obj.version,
|
||
description: !!obj.description
|
||
};
|
||
|
||
console.log(`Modal: Extension property analysis:`, extensionPropChecks);
|
||
|
||
const hasExtensionProps = !!(
|
||
obj._isEnabled || obj.enabled || obj.kind ||
|
||
obj._eventEmitter || obj._scope || obj._requests || obj._pubkey ||
|
||
obj.name || obj.version || obj.description
|
||
);
|
||
|
||
const underscoreKeys = objectKeys.filter(key => key.startsWith('_'));
|
||
const hexToUint8Keys = objectKeys.filter(key => key.startsWith('_hex'));
|
||
console.log(`Modal: Underscore keys: [${underscoreKeys.join(', ')}]`);
|
||
console.log(`Modal: _hex* keys: [${hexToUint8Keys.join(', ')}]`);
|
||
|
||
console.log(`Modal: Additional analysis:`);
|
||
console.log(` hasExtensionProps: ${hasExtensionProps}`);
|
||
console.log(` hasLibraryMethod (_hexToUint8Array): ${objectKeys.includes('_hexToUint8Array')}`);
|
||
|
||
console.log(`Modal: COMPREHENSIVE TEST LOGIC RESULT: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`);
|
||
console.log(`Modal: FINAL DECISION for ${constructorName}: ${isRealExtension ? 'ACCEPT' : 'REJECT'}`);
|
||
|
||
return isRealExtension;
|
||
}
|
||
|
||
_showExtensionChoice(extensions) {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = 'Choose Browser Extension';
|
||
title.style.cssText = `
|
||
margin: 0 0 16px 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--nl-primary-color);
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
const description = document.createElement('p');
|
||
description.textContent = `Found ${extensions.length} Nostr extensions. Choose which one to use:`;
|
||
description.style.cssText = `
|
||
margin-bottom: 20px;
|
||
color: #666666;
|
||
font-size: 14px;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
this.modalBody.appendChild(title);
|
||
this.modalBody.appendChild(description);
|
||
|
||
// Create button for each extension
|
||
extensions.forEach((ext, index) => {
|
||
const button = document.createElement('button');
|
||
button.onclick = () => this._tryExtensionLogin(ext.extension);
|
||
button.style.cssText = `
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-align: left;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
button.onmouseover = () => {
|
||
button.style.borderColor = 'var(--nl-accent-color)';
|
||
button.style.background = 'var(--nl-secondary-color)';
|
||
};
|
||
button.onmouseout = () => {
|
||
button.style.borderColor = 'var(--nl-primary-color)';
|
||
button.style.background = 'var(--nl-secondary-color)';
|
||
};
|
||
|
||
const iconDiv = document.createElement('div');
|
||
iconDiv.textContent = ext.icon;
|
||
iconDiv.style.cssText = `
|
||
font-size: 24px;
|
||
margin-right: 16px;
|
||
width: 24px;
|
||
text-align: center;
|
||
`;
|
||
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.style.cssText = 'flex: 1;';
|
||
|
||
const nameDiv = document.createElement('div');
|
||
nameDiv.textContent = ext.displayName;
|
||
nameDiv.style.cssText = `
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
color: var(--nl-primary-color);
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
const pathDiv = document.createElement('div');
|
||
pathDiv.textContent = ext.name;
|
||
pathDiv.style.cssText = `
|
||
font-size: 12px;
|
||
color: #666666;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
contentDiv.appendChild(nameDiv);
|
||
contentDiv.appendChild(pathDiv);
|
||
|
||
button.appendChild(iconDiv);
|
||
button.appendChild(contentDiv);
|
||
this.modalBody.appendChild(button);
|
||
});
|
||
|
||
// Add back button
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back to Login Options';
|
||
backButton.onclick = () => this._renderLoginOptions();
|
||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 20px;';
|
||
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
async _tryExtensionLogin(extensionObj) {
|
||
try {
|
||
// Show loading state
|
||
this.modalBody.innerHTML = '<div style="text-align: center; padding: 20px;">🔄 Connecting to extension...</div>';
|
||
|
||
// Get pubkey from extension
|
||
const pubkey = await extensionObj.getPublicKey();
|
||
console.log('Extension provided pubkey:', pubkey);
|
||
|
||
// Set extension method with the extension object
|
||
this._setAuthMethod('extension', { pubkey, extension: extensionObj });
|
||
|
||
} catch (error) {
|
||
console.error('Extension login failed:', error);
|
||
this._showError(`Extension login failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
_showLocalKeyScreen() {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = 'Local Key';
|
||
title.style.cssText = 'margin: 0 0 20px 0; font-size: 18px; font-weight: 600;';
|
||
|
||
const createButton = document.createElement('button');
|
||
createButton.textContent = 'Create New Key';
|
||
createButton.onclick = () => this._createLocalKey();
|
||
createButton.style.cssText = this._getButtonStyle();
|
||
|
||
const importButton = document.createElement('button');
|
||
importButton.textContent = 'Import Existing Key';
|
||
importButton.onclick = () => this._showImportKeyForm();
|
||
importButton.style.cssText = this._getButtonStyle() + 'margin-top: 12px;';
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back';
|
||
backButton.onclick = () => this._renderLoginOptions();
|
||
backButton.style.cssText = `
|
||
display: block;
|
||
margin-top: 20px;
|
||
padding: 12px;
|
||
background: #6b7280;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
`;
|
||
|
||
this.modalBody.appendChild(title);
|
||
this.modalBody.appendChild(createButton);
|
||
this.modalBody.appendChild(importButton);
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
_createLocalKey() {
|
||
try {
|
||
const sk = window.NostrTools.generateSecretKey();
|
||
const pk = window.NostrTools.getPublicKey(sk);
|
||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||
const npub = window.NostrTools.nip19.npubEncode(pk);
|
||
|
||
this._showKeyDisplay(pk, nsec, 'created');
|
||
} catch (error) {
|
||
this._showError('Failed to create key: ' + error.message);
|
||
}
|
||
}
|
||
|
||
_showImportKeyForm() {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = 'Import Local Key';
|
||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||
|
||
const description = document.createElement('p');
|
||
description.textContent = 'Enter your secret key in either nsec or hex format:';
|
||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.placeholder = 'Enter your secret key:\n• nsec1... (bech32 format)\n• 64-character hex string';
|
||
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 format detection
|
||
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 format = this._detectKeyFormat(value);
|
||
if (format === 'nsec') {
|
||
formatHint.textContent = '✅ Valid nsec format detected';
|
||
formatHint.style.color = '#059669';
|
||
} else if (format === 'hex') {
|
||
formatHint.textContent = '✅ Valid hex format detected';
|
||
formatHint.style.color = '#059669';
|
||
} else {
|
||
formatHint.textContent = '❌ Invalid key format - must be nsec1... or 64-character hex';
|
||
formatHint.style.color = '#dc2626';
|
||
}
|
||
};
|
||
|
||
const importButton = document.createElement('button');
|
||
importButton.textContent = 'Import Key';
|
||
importButton.onclick = () => this._importLocalKey(textarea.value);
|
||
importButton.style.cssText = this._getButtonStyle();
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back';
|
||
backButton.onclick = () => this._showLocalKeyScreen();
|
||
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(importButton);
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
_detectKeyFormat(keyValue) {
|
||
const trimmed = keyValue.trim();
|
||
|
||
// Check for nsec format
|
||
if (trimmed.startsWith('nsec1') && trimmed.length === 63) {
|
||
try {
|
||
window.NostrTools.nip19.decode(trimmed);
|
||
return 'nsec';
|
||
} catch {
|
||
return 'invalid';
|
||
}
|
||
}
|
||
|
||
// Check for hex format (64 characters, valid hex)
|
||
if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) {
|
||
return 'hex';
|
||
}
|
||
|
||
return 'invalid';
|
||
}
|
||
|
||
_importLocalKey(keyValue) {
|
||
try {
|
||
const trimmed = keyValue.trim();
|
||
if (!trimmed) {
|
||
throw new Error('Please enter a secret key');
|
||
}
|
||
|
||
const format = this._detectKeyFormat(trimmed);
|
||
let sk;
|
||
|
||
if (format === 'nsec') {
|
||
// Decode nsec format - this returns Uint8Array
|
||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||
if (decoded.type !== 'nsec') {
|
||
throw new Error('Invalid nsec format');
|
||
}
|
||
sk = decoded.data; // This is already Uint8Array
|
||
} else if (format === 'hex') {
|
||
// Convert hex string to Uint8Array
|
||
sk = this._hexToUint8Array(trimmed);
|
||
// Test that it's a valid secret key by trying to get public key
|
||
window.NostrTools.getPublicKey(sk);
|
||
} else {
|
||
throw new Error('Invalid key format. Please enter either nsec1... or 64-character hex string');
|
||
}
|
||
|
||
// Generate public key and encoded formats
|
||
const pk = window.NostrTools.getPublicKey(sk);
|
||
const nsec = window.NostrTools.nip19.nsecEncode(sk);
|
||
const npub = window.NostrTools.nip19.npubEncode(pk);
|
||
|
||
this._showKeyDisplay(pk, nsec, 'imported');
|
||
} catch (error) {
|
||
this._showError('Invalid key: ' + error.message);
|
||
}
|
||
}
|
||
|
||
_hexToUint8Array(hex) {
|
||
// Convert hex string to Uint8Array
|
||
if (hex.length % 2 !== 0) {
|
||
throw new Error('Invalid hex string length');
|
||
}
|
||
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;
|
||
}
|
||
|
||
_showKeyDisplay(pubkey, nsec, action) {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = `Key ${action} successfully!`;
|
||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;';
|
||
|
||
const warningDiv = document.createElement('div');
|
||
warningDiv.textContent = '⚠️ Save your secret key securely!';
|
||
warningDiv.style.cssText = 'background: #fef3c7; color: #92400e; padding: 12px; border-radius: 6px; margin-bottom: 16px; font-size: 12px;';
|
||
|
||
// Helper function to create copy button
|
||
const createCopyButton = (text, label) => {
|
||
const copyBtn = document.createElement('button');
|
||
copyBtn.textContent = `Copy ${label}`;
|
||
copyBtn.style.cssText = `
|
||
margin-left: 8px;
|
||
padding: 4px 8px;
|
||
font-size: 10px;
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: 1px solid var(--nl-primary-color);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
copyBtn.onclick = async (e) => {
|
||
e.preventDefault();
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
const originalText = copyBtn.textContent;
|
||
copyBtn.textContent = '✓ Copied!';
|
||
copyBtn.style.color = '#059669';
|
||
setTimeout(() => {
|
||
copyBtn.textContent = originalText;
|
||
copyBtn.style.color = 'var(--nl-primary-color)';
|
||
}, 2000);
|
||
} catch (err) {
|
||
console.error('Failed to copy:', err);
|
||
copyBtn.textContent = '✗ Failed';
|
||
copyBtn.style.color = '#dc2626';
|
||
setTimeout(() => {
|
||
copyBtn.textContent = originalText;
|
||
copyBtn.style.color = 'var(--nl-primary-color)';
|
||
}, 2000);
|
||
}
|
||
};
|
||
return copyBtn;
|
||
};
|
||
|
||
// Convert pubkey to hex for verification
|
||
const pubkeyHex = typeof pubkey === 'string' ? pubkey : Array.from(pubkey).map(b => b.toString(16).padStart(2, '0')).join('');
|
||
|
||
// Decode nsec to get secret key as hex
|
||
let secretKeyHex = '';
|
||
try {
|
||
const decoded = window.NostrTools.nip19.decode(nsec);
|
||
secretKeyHex = Array.from(decoded.data).map(b => b.toString(16).padStart(2, '0')).join('');
|
||
} catch (err) {
|
||
console.error('Failed to decode nsec for hex display:', err);
|
||
}
|
||
|
||
// Secret Key Section
|
||
const nsecSection = document.createElement('div');
|
||
nsecSection.style.cssText = 'margin-bottom: 16px;';
|
||
|
||
const nsecLabel = document.createElement('div');
|
||
nsecLabel.innerHTML = '<strong>Your Secret Key (nsec):</strong>';
|
||
nsecLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||
|
||
const nsecContainer = document.createElement('div');
|
||
nsecContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||
|
||
const nsecCode = document.createElement('code');
|
||
nsecCode.textContent = nsec;
|
||
nsecCode.style.cssText = `
|
||
flex: 1;
|
||
word-break: break-all;
|
||
background: #f3f4f6;
|
||
padding: 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
line-height: 1.3;
|
||
font-family: 'Courier New', monospace;
|
||
display: block;
|
||
`;
|
||
|
||
nsecContainer.appendChild(nsecCode);
|
||
nsecContainer.appendChild(createCopyButton(nsec, 'nsec'));
|
||
nsecSection.appendChild(nsecLabel);
|
||
nsecSection.appendChild(nsecContainer);
|
||
|
||
// Secret Key Hex Section
|
||
if (secretKeyHex) {
|
||
const secretHexLabel = document.createElement('div');
|
||
secretHexLabel.innerHTML = '<strong>Secret Key (hex):</strong>';
|
||
secretHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||
|
||
const secretHexContainer = document.createElement('div');
|
||
secretHexContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||
|
||
const secretHexCode = document.createElement('code');
|
||
secretHexCode.textContent = secretKeyHex;
|
||
secretHexCode.style.cssText = `
|
||
flex: 1;
|
||
word-break: break-all;
|
||
background: #f3f4f6;
|
||
padding: 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
line-height: 1.3;
|
||
font-family: 'Courier New', monospace;
|
||
display: block;
|
||
`;
|
||
|
||
secretHexContainer.appendChild(secretHexCode);
|
||
secretHexContainer.appendChild(createCopyButton(secretKeyHex, 'hex'));
|
||
nsecSection.appendChild(secretHexLabel);
|
||
nsecSection.appendChild(secretHexContainer);
|
||
}
|
||
|
||
// Public Key Section
|
||
const npubSection = document.createElement('div');
|
||
npubSection.style.cssText = 'margin-bottom: 16px;';
|
||
|
||
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
||
|
||
const npubLabel = document.createElement('div');
|
||
npubLabel.innerHTML = '<strong>Your Public Key (npub):</strong>';
|
||
npubLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||
|
||
const npubContainer = document.createElement('div');
|
||
npubContainer.style.cssText = 'display: flex; align-items: flex-start; margin-bottom: 8px;';
|
||
|
||
const npubCode = document.createElement('code');
|
||
npubCode.textContent = npub;
|
||
npubCode.style.cssText = `
|
||
flex: 1;
|
||
word-break: break-all;
|
||
background: #f3f4f6;
|
||
padding: 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
line-height: 1.3;
|
||
font-family: 'Courier New', monospace;
|
||
display: block;
|
||
`;
|
||
|
||
npubContainer.appendChild(npubCode);
|
||
npubContainer.appendChild(createCopyButton(npub, 'npub'));
|
||
npubSection.appendChild(npubLabel);
|
||
npubSection.appendChild(npubContainer);
|
||
|
||
// Public Key Hex Section
|
||
const pubHexLabel = document.createElement('div');
|
||
pubHexLabel.innerHTML = '<strong>Public Key (hex):</strong>';
|
||
pubHexLabel.style.cssText = 'margin-bottom: 4px; font-size: 12px; font-weight: 600;';
|
||
|
||
const pubHexContainer = document.createElement('div');
|
||
pubHexContainer.style.cssText = 'display: flex; align-items: flex-start;';
|
||
|
||
const pubHexCode = document.createElement('code');
|
||
pubHexCode.textContent = pubkeyHex;
|
||
pubHexCode.style.cssText = `
|
||
flex: 1;
|
||
word-break: break-all;
|
||
background: #f3f4f6;
|
||
padding: 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
line-height: 1.3;
|
||
font-family: 'Courier New', monospace;
|
||
display: block;
|
||
`;
|
||
|
||
pubHexContainer.appendChild(pubHexCode);
|
||
pubHexContainer.appendChild(createCopyButton(pubkeyHex, 'hex'));
|
||
npubSection.appendChild(pubHexLabel);
|
||
npubSection.appendChild(pubHexContainer);
|
||
|
||
const continueButton = document.createElement('button');
|
||
continueButton.textContent = 'Continue';
|
||
continueButton.onclick = () => this._setAuthMethod('local', { secret: nsec, pubkey });
|
||
continueButton.style.cssText = this._getButtonStyle();
|
||
|
||
this.modalBody.appendChild(title);
|
||
this.modalBody.appendChild(warningDiv);
|
||
this.modalBody.appendChild(nsecSection);
|
||
this.modalBody.appendChild(npubSection);
|
||
this.modalBody.appendChild(continueButton);
|
||
}
|
||
|
||
_setAuthMethod(method, options = {}) {
|
||
// SINGLE-EXTENSION ARCHITECTURE: Handle method switching
|
||
console.log('Modal: _setAuthMethod called with:', method, options);
|
||
|
||
// CRITICAL: Never install facade for extension methods - leave window.nostr as the extension
|
||
if (method === 'extension') {
|
||
console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension');
|
||
|
||
// Save extension authentication state using global setAuthState function
|
||
if (typeof window.setAuthState === 'function') {
|
||
console.log('Modal: Saving extension auth state to storage');
|
||
window.setAuthState({ method, ...options }, { isolateSession: this.options?.isolateSession });
|
||
}
|
||
|
||
// Emit auth method selection directly for extension
|
||
const event = new CustomEvent('nlMethodSelected', {
|
||
detail: { method, ...options }
|
||
});
|
||
window.dispatchEvent(event);
|
||
|
||
this.close();
|
||
return;
|
||
}
|
||
|
||
// For non-extension methods, we need to ensure WindowNostr facade is available
|
||
console.log('Modal: Non-extension method detected:', method);
|
||
|
||
// Check if we have a preserved extension but no WindowNostr facade installed
|
||
const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension;
|
||
const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr';
|
||
|
||
console.log('Modal: Method switching check:');
|
||
console.log(' method:', method);
|
||
console.log(' hasPreservedExtension:', hasPreservedExtension);
|
||
console.log(' hasWindowNostrFacade:', hasWindowNostrFacade);
|
||
console.log(' current window.nostr constructor:', window.nostr?.constructor?.name);
|
||
|
||
// If we have a preserved extension but no facade, install facade for method switching
|
||
if (hasPreservedExtension && !hasWindowNostrFacade) {
|
||
console.log('Modal: Installing WindowNostr facade for method switching (non-extension authentication)');
|
||
|
||
// Get the NostrLite instance and install facade with preserved extension
|
||
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
||
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
||
const preservedExtension = nostrLiteInstance.preservedExtension;
|
||
console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name);
|
||
|
||
nostrLiteInstance._installFacade(preservedExtension);
|
||
console.log('Modal: WindowNostr facade installed for method switching');
|
||
} else {
|
||
console.error('Modal: Cannot access NostrLite instance or _installFacade method');
|
||
}
|
||
}
|
||
|
||
// If no extension at all, ensure facade is installed for local/NIP-46/readonly methods
|
||
else if (!hasPreservedExtension && !hasWindowNostrFacade) {
|
||
console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)');
|
||
|
||
const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance;
|
||
if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') {
|
||
nostrLiteInstance._installFacade();
|
||
console.log('Modal: WindowNostr facade installed for non-extension methods');
|
||
}
|
||
}
|
||
|
||
// Emit auth method selection
|
||
const event = new CustomEvent('nlMethodSelected', {
|
||
detail: { method, ...options }
|
||
});
|
||
window.dispatchEvent(event);
|
||
|
||
this.close();
|
||
}
|
||
|
||
_showError(message) {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.style.cssText = 'background: #fee2e2; color: #dc2626; padding: 16px; border-radius: 6px; margin-bottom: 16px;';
|
||
errorDiv.innerHTML = `<strong>Error:</strong> ${message}`;
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back';
|
||
backButton.onclick = () => this._renderLoginOptions();
|
||
backButton.style.cssText = this._getButtonStyle('secondary');
|
||
|
||
this.modalBody.appendChild(errorDiv);
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
_showExtensionRequired() {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = 'Browser Extension Required';
|
||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||
|
||
const message = document.createElement('p');
|
||
message.innerHTML = `
|
||
Please install a Nostr browser extension and refresh the page.<br><br>
|
||
<strong>Important:</strong> If you have multiple extensions installed, please disable all but one to avoid conflicts.
|
||
<br><br>
|
||
Popular extensions: Alby, nos2x, Flamingo
|
||
`;
|
||
message.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px; line-height: 1.4;';
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back';
|
||
backButton.onclick = () => this._renderLoginOptions();
|
||
backButton.style.cssText = this._getButtonStyle('secondary');
|
||
|
||
this.modalBody.appendChild(title);
|
||
this.modalBody.appendChild(message);
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
_showConnectScreen() {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const description = document.createElement('p');
|
||
description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.';
|
||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||
|
||
const formGroup = document.createElement('div');
|
||
formGroup.style.cssText = 'margin-bottom: 20px;';
|
||
|
||
const label = document.createElement('label');
|
||
label.textContent = 'Bunker Public Key:';
|
||
label.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
|
||
|
||
const pubkeyInput = document.createElement('input');
|
||
pubkeyInput.type = 'text';
|
||
pubkeyInput.placeholder = 'bunker://pubkey?relay=..., bunker:hex, hex, or npub...';
|
||
pubkeyInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 6px;
|
||
margin-bottom: 12px;
|
||
font-family: monospace;
|
||
box-sizing: border-box;
|
||
`;
|
||
|
||
// Add real-time bunker key validation
|
||
const formatHint = document.createElement('div');
|
||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||
|
||
const connectButton = document.createElement('button');
|
||
connectButton.textContent = 'Connect to Bunker';
|
||
connectButton.disabled = true;
|
||
connectButton.onclick = () => {
|
||
if (!connectButton.disabled) {
|
||
this._handleNip46Connect(pubkeyInput.value);
|
||
}
|
||
};
|
||
|
||
// Set initial disabled state
|
||
connectButton.style.cssText = `
|
||
display: block;
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||
border-radius: var(--nl-border-radius);
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
cursor: not-allowed;
|
||
transition: all 0.2s;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-muted-color);
|
||
margin-bottom: 12px;
|
||
`;
|
||
|
||
pubkeyInput.oninput = () => {
|
||
const value = pubkeyInput.value.trim();
|
||
if (!value) {
|
||
formatHint.textContent = '';
|
||
// Disable button
|
||
connectButton.disabled = true;
|
||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||
connectButton.style.color = 'var(--nl-muted-color)';
|
||
connectButton.style.cursor = 'not-allowed';
|
||
return;
|
||
}
|
||
|
||
const isValid = this._validateBunkerKey(value);
|
||
if (isValid) {
|
||
formatHint.textContent = '✅ Valid bunker connection format detected';
|
||
formatHint.style.color = '#059669';
|
||
// Enable button
|
||
connectButton.disabled = false;
|
||
connectButton.style.borderColor = 'var(--nl-primary-color)';
|
||
connectButton.style.color = 'var(--nl-primary-color)';
|
||
connectButton.style.cursor = 'pointer';
|
||
} else {
|
||
formatHint.textContent = '❌ Invalid format - must be bunker://, npub, or 64-char hex';
|
||
formatHint.style.color = '#dc2626';
|
||
// Disable button
|
||
connectButton.disabled = true;
|
||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||
connectButton.style.color = 'var(--nl-muted-color)';
|
||
connectButton.style.cursor = 'not-allowed';
|
||
}
|
||
};
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back';
|
||
backButton.onclick = () => this._renderLoginOptions();
|
||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||
|
||
formGroup.appendChild(label);
|
||
formGroup.appendChild(pubkeyInput);
|
||
formGroup.appendChild(formatHint);
|
||
|
||
this.modalBody.appendChild(description);
|
||
this.modalBody.appendChild(formGroup);
|
||
this.modalBody.appendChild(connectButton);
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
_validateBunkerKey(bunkerKey) {
|
||
try {
|
||
const trimmed = bunkerKey.trim();
|
||
|
||
// Check for bunker:// format
|
||
if (trimmed.startsWith('bunker://')) {
|
||
// Should have format: bunker://pubkey or bunker://pubkey?param=value
|
||
const match = trimmed.match(/^bunker:\/\/([0-9a-fA-F]{64})(\?.*)?$/);
|
||
return !!match;
|
||
}
|
||
|
||
// Check for npub format
|
||
if (trimmed.startsWith('npub1') && trimmed.length === 63) {
|
||
try {
|
||
if (window.NostrTools?.nip19) {
|
||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||
return decoded.type === 'npub';
|
||
}
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Check for hex format (64 characters, valid hex)
|
||
if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
} catch (error) {
|
||
console.log('Bunker key validation failed:', error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
_handleNip46Connect(bunkerPubkey) {
|
||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||
this._showError('Bunker pubkey is required');
|
||
return;
|
||
}
|
||
|
||
this._showNip46Connecting(bunkerPubkey);
|
||
this._performNip46Connect(bunkerPubkey);
|
||
}
|
||
|
||
_showNip46Connecting(bunkerPubkey) {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = 'Connecting to Remote Signer...';
|
||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;';
|
||
|
||
const description = document.createElement('p');
|
||
description.textContent = 'Establishing secure connection to your remote signer.';
|
||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
|
||
|
||
// Normalize bunker pubkey for display (= show original format if bunker: prefix)
|
||
const displayPubkey = bunkerPubkey.startsWith('bunker:') || bunkerPubkey.startsWith('npub') || bunkerPubkey.length === 64 ? bunkerPubkey : bunkerPubkey;
|
||
|
||
const bunkerInfo = document.createElement('div');
|
||
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
|
||
bunkerInfo.innerHTML = `
|
||
<strong>Connecting to bunker:</strong><br>
|
||
Connection: <code style="word-break: break-all;">${displayPubkey}</code><br>
|
||
<small style="color: #6b7280;">Connection string contains all necessary relay information.</small>
|
||
`;
|
||
|
||
const connectingDiv = document.createElement('div');
|
||
connectingDiv.style.cssText = 'text-align: center; color: #6b7280;';
|
||
connectingDiv.innerHTML = `
|
||
<div style="font-size: 24px; margin-bottom: 10px;">⏳</div>
|
||
<div>Please wait while we establish the connection...</div>
|
||
<div style="font-size: 12px; margin-top: 10px;">This may take a few seconds</div>
|
||
`;
|
||
|
||
this.modalBody.appendChild(title);
|
||
this.modalBody.appendChild(description);
|
||
this.modalBody.appendChild(bunkerInfo);
|
||
this.modalBody.appendChild(connectingDiv);
|
||
}
|
||
|
||
async _performNip46Connect(bunkerPubkey) {
|
||
try {
|
||
console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
|
||
|
||
// Check if nostr-tools NIP-46 is available
|
||
if (!window.NostrTools?.nip46) {
|
||
throw new Error('nostr-tools NIP-46 module not available');
|
||
}
|
||
|
||
// Use nostr-tools to parse bunker input - this handles all formats correctly
|
||
console.log('Parsing bunker input with nostr-tools...');
|
||
const bunkerPointer = await window.NostrTools.nip46.parseBunkerInput(bunkerPubkey);
|
||
|
||
if (!bunkerPointer) {
|
||
throw new Error('Unable to parse bunker connection string or resolve NIP-05 identifier');
|
||
}
|
||
|
||
console.log('Parsed bunker pointer:', bunkerPointer);
|
||
|
||
// Create local client keypair for this session
|
||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||
console.log('Generated local client keypair for NIP-46 session');
|
||
|
||
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
||
console.log('Creating nip46 BunkerSigner...');
|
||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||
onauth: (url) => {
|
||
console.log('Received auth URL from bunker:', url);
|
||
// Open auth URL in popup or redirect
|
||
window.open(url, '_blank', 'width=600,height=800');
|
||
}
|
||
});
|
||
|
||
console.log('NIP-46 BunkerSigner created successfully');
|
||
|
||
// Skip ping test - NIP-46 works through relays, not direct connection
|
||
// Try to connect directly (this may trigger auth flow)
|
||
console.log('Attempting NIP-46 connect...');
|
||
await signer.connect();
|
||
console.log('NIP-46 connect successful');
|
||
|
||
// Get the user's public key from the bunker
|
||
console.log('Getting public key from bunker...');
|
||
const userPubkey = await signer.getPublicKey();
|
||
console.log('NIP-46 user public key:', userPubkey);
|
||
|
||
// Store the NIP-46 authentication info
|
||
const nip46Info = {
|
||
pubkey: userPubkey,
|
||
signer: {
|
||
method: 'nip46',
|
||
remotePubkey: bunkerPointer.pubkey,
|
||
bunkerSigner: signer,
|
||
secret: bunkerPointer.secret,
|
||
relays: bunkerPointer.relays
|
||
}
|
||
};
|
||
|
||
console.log('NOSTR_LOGIN_LITE NIP-46 connection established successfully!');
|
||
|
||
// Set as current auth method
|
||
this._setAuthMethod('nip46', nip46Info);
|
||
return;
|
||
|
||
} catch (error) {
|
||
console.error('NIP-46 connection failed:', error);
|
||
this._showNip46Error(error.message);
|
||
}
|
||
}
|
||
|
||
_showNip46Error(message) {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = 'Connection Failed';
|
||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #dc2626;';
|
||
|
||
const errorMsg = document.createElement('p');
|
||
errorMsg.textContent = `Unable to connect to remote signer: ${message}`;
|
||
errorMsg.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
|
||
|
||
const retryButton = document.createElement('button');
|
||
retryButton.textContent = 'Try Again';
|
||
retryButton.onclick = () => this._showConnectScreen();
|
||
retryButton.style.cssText = this._getButtonStyle();
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back to Options';
|
||
backButton.onclick = () => this._renderLoginOptions();
|
||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||
|
||
this.modalBody.appendChild(title);
|
||
this.modalBody.appendChild(errorMsg);
|
||
this.modalBody.appendChild(retryButton);
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
_handleReadonly() {
|
||
// Set read-only mode
|
||
this._setAuthMethod('readonly');
|
||
}
|
||
|
||
_showSeedPhraseScreen() {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const description = document.createElement('p');
|
||
description.innerHTML = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts, or <span id="generate-new" style="text-decoration: underline; cursor: pointer; color: var(--nl-primary-color);">generate new</span>.';
|
||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
// Remove default placeholder text as requested
|
||
textarea.placeholder = '';
|
||
textarea.style.cssText = `
|
||
width: 100%;
|
||
height: 100px;
|
||
padding: 12px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 6px;
|
||
margin-bottom: 12px;
|
||
resize: none;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
box-sizing: border-box;
|
||
`;
|
||
|
||
// Add real-time mnemonic validation
|
||
const formatHint = document.createElement('div');
|
||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||
|
||
const importButton = document.createElement('button');
|
||
importButton.textContent = 'Import Accounts';
|
||
importButton.disabled = true;
|
||
importButton.onclick = () => {
|
||
if (!importButton.disabled) {
|
||
this._importFromSeedPhrase(textarea.value);
|
||
}
|
||
};
|
||
|
||
// Set initial disabled state
|
||
importButton.style.cssText = `
|
||
display: block;
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||
border-radius: var(--nl-border-radius);
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
cursor: not-allowed;
|
||
transition: all 0.2s;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-muted-color);
|
||
`;
|
||
|
||
textarea.oninput = () => {
|
||
const value = textarea.value.trim();
|
||
if (!value) {
|
||
formatHint.textContent = '';
|
||
// Disable button
|
||
importButton.disabled = true;
|
||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||
importButton.style.color = 'var(--nl-muted-color)';
|
||
importButton.style.cursor = 'not-allowed';
|
||
return;
|
||
}
|
||
|
||
const isValid = this._validateMnemonic(value);
|
||
if (isValid) {
|
||
const wordCount = value.split(/\s+/).length;
|
||
formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`;
|
||
formatHint.style.color = '#059669';
|
||
// Enable button
|
||
importButton.disabled = false;
|
||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||
importButton.style.color = 'var(--nl-primary-color)';
|
||
importButton.style.cursor = 'pointer';
|
||
} else {
|
||
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
|
||
formatHint.style.color = '#dc2626';
|
||
// Disable button
|
||
importButton.disabled = true;
|
||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||
importButton.style.color = 'var(--nl-muted-color)';
|
||
importButton.style.cursor = 'not-allowed';
|
||
}
|
||
};
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back';
|
||
backButton.onclick = () => this._renderLoginOptions();
|
||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||
|
||
this.modalBody.appendChild(description);
|
||
this.modalBody.appendChild(textarea);
|
||
this.modalBody.appendChild(formatHint);
|
||
this.modalBody.appendChild(importButton);
|
||
this.modalBody.appendChild(backButton);
|
||
|
||
// Add click handler for the "generate new" link
|
||
const generateLink = document.getElementById('generate-new');
|
||
if (generateLink) {
|
||
generateLink.addEventListener('mouseenter', () => {
|
||
generateLink.style.color = 'var(--nl-accent-color)';
|
||
});
|
||
generateLink.addEventListener('mouseleave', () => {
|
||
generateLink.style.color = 'var(--nl-primary-color)';
|
||
});
|
||
generateLink.addEventListener('click', () => {
|
||
this._generateNewSeedPhrase(textarea, formatHint);
|
||
});
|
||
}
|
||
}
|
||
|
||
_generateNewSeedPhrase(textarea, formatHint) {
|
||
try {
|
||
// Check if NIP-06 is available
|
||
if (!window.NostrTools?.nip06) {
|
||
throw new Error('NIP-06 not available in bundle');
|
||
}
|
||
|
||
// Generate a random 12-word mnemonic using NostrTools
|
||
const mnemonic = window.NostrTools.nip06.generateSeedWords();
|
||
|
||
// Set the generated mnemonic in the textarea
|
||
textarea.value = mnemonic;
|
||
|
||
// Trigger the oninput event to properly validate and enable the button
|
||
if (textarea.oninput) {
|
||
textarea.oninput();
|
||
}
|
||
|
||
console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words');
|
||
|
||
} catch (error) {
|
||
console.error('Failed to generate seed phrase:', error);
|
||
formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available';
|
||
formatHint.style.color = '#dc2626';
|
||
}
|
||
}
|
||
|
||
_validateMnemonic(mnemonic) {
|
||
try {
|
||
// Check if NIP-06 is available
|
||
if (!window.NostrTools?.nip06) {
|
||
console.error('NIP-06 not available in bundle');
|
||
return false;
|
||
}
|
||
|
||
const words = mnemonic.trim().split(/\s+/);
|
||
|
||
// Must be 12 or 24 words
|
||
if (words.length !== 12 && words.length !== 24) {
|
||
return false;
|
||
}
|
||
|
||
// Try to validate using NostrTools nip06 - this will throw if invalid
|
||
window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0);
|
||
return true;
|
||
} catch (error) {
|
||
console.log('Mnemonic validation failed:', error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
_importFromSeedPhrase(mnemonic) {
|
||
try {
|
||
const trimmed = mnemonic.trim();
|
||
if (!trimmed) {
|
||
throw new Error('Please enter a mnemonic seed phrase');
|
||
}
|
||
|
||
// Validate the mnemonic
|
||
if (!this._validateMnemonic(trimmed)) {
|
||
throw new Error('Invalid mnemonic. Please enter a valid 12 or 24-word BIP-39 seed phrase');
|
||
}
|
||
|
||
// Generate accounts 0-5 using NIP-06
|
||
const accounts = [];
|
||
for (let i = 0; i < 6; i++) {
|
||
try {
|
||
const privateKey = window.NostrTools.nip06.privateKeyFromSeedWords(trimmed, '', i);
|
||
const publicKey = window.NostrTools.getPublicKey(privateKey);
|
||
const nsec = window.NostrTools.nip19.nsecEncode(privateKey);
|
||
const npub = window.NostrTools.nip19.npubEncode(publicKey);
|
||
|
||
accounts.push({
|
||
index: i,
|
||
privateKey,
|
||
publicKey,
|
||
nsec,
|
||
npub
|
||
});
|
||
} catch (error) {
|
||
console.error(`Failed to derive account ${i}:`, error);
|
||
}
|
||
}
|
||
|
||
if (accounts.length === 0) {
|
||
throw new Error('Failed to derive any accounts from seed phrase');
|
||
}
|
||
|
||
console.log(`Successfully derived ${accounts.length} accounts from seed phrase`);
|
||
this._showAccountSelection(accounts);
|
||
|
||
} catch (error) {
|
||
console.error('Seed phrase import failed:', error);
|
||
this._showError('Seed phrase import failed: ' + error.message);
|
||
}
|
||
}
|
||
|
||
_showAccountSelection(accounts) {
|
||
this.modalBody.innerHTML = '';
|
||
|
||
const description = document.createElement('p');
|
||
description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`;
|
||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||
|
||
this.modalBody.appendChild(description);
|
||
|
||
// Create table for account selection
|
||
const table = document.createElement('table');
|
||
table.style.cssText = `
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-bottom: 20px;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
font-size: 12px;
|
||
`;
|
||
|
||
// Table header
|
||
const thead = document.createElement('thead');
|
||
thead.innerHTML = `
|
||
<tr style="background: #f3f4f6;">
|
||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">#</th>
|
||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Use</th>
|
||
</tr>
|
||
`;
|
||
table.appendChild(thead);
|
||
|
||
// Table body
|
||
const tbody = document.createElement('tbody');
|
||
accounts.forEach(account => {
|
||
const row = document.createElement('tr');
|
||
row.style.cssText = 'border: 1px solid #d1d5db;';
|
||
|
||
const indexCell = document.createElement('td');
|
||
indexCell.textContent = account.index;
|
||
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
|
||
|
||
const actionCell = document.createElement('td');
|
||
actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;';
|
||
|
||
// Show truncated npub in the button
|
||
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||
|
||
const selectButton = document.createElement('button');
|
||
selectButton.textContent = truncatedNpub;
|
||
selectButton.onclick = () => this._selectAccount(account);
|
||
selectButton.style.cssText = `
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
font-size: 11px;
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
border: 1px solid var(--nl-primary-color);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-family: 'Courier New', monospace;
|
||
text-align: center;
|
||
`;
|
||
selectButton.onmouseover = () => {
|
||
selectButton.style.borderColor = 'var(--nl-accent-color)';
|
||
};
|
||
selectButton.onmouseout = () => {
|
||
selectButton.style.borderColor = 'var(--nl-primary-color)';
|
||
};
|
||
|
||
actionCell.appendChild(selectButton);
|
||
|
||
row.appendChild(indexCell);
|
||
row.appendChild(actionCell);
|
||
tbody.appendChild(row);
|
||
});
|
||
table.appendChild(tbody);
|
||
|
||
this.modalBody.appendChild(table);
|
||
|
||
// Back button
|
||
const backButton = document.createElement('button');
|
||
backButton.textContent = 'Back to Seed Phrase';
|
||
backButton.onclick = () => this._showSeedPhraseScreen();
|
||
backButton.style.cssText = this._getButtonStyle('secondary');
|
||
|
||
this.modalBody.appendChild(backButton);
|
||
}
|
||
|
||
_selectAccount(account) {
|
||
console.log('Selected account:', account.index, account.npub);
|
||
|
||
// Use the same auth method as local keys, but with seedphrase identifier
|
||
this._setAuthMethod('local', {
|
||
secret: account.nsec,
|
||
pubkey: account.publicKey,
|
||
source: 'seedphrase',
|
||
accountIndex: account.index
|
||
});
|
||
}
|
||
|
||
_showOtpScreen() {
|
||
// Placeholder for OTP functionality
|
||
this._showError('OTP/DM not yet implemented - coming soon!');
|
||
}
|
||
|
||
_getButtonStyle(type = 'primary') {
|
||
const baseStyle = `
|
||
display: block;
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||
`;
|
||
|
||
if (type === 'primary') {
|
||
return baseStyle + `
|
||
background: var(--nl-secondary-color);
|
||
color: var(--nl-primary-color);
|
||
`;
|
||
} else {
|
||
return baseStyle + `
|
||
background: #cccccc;
|
||
color: var(--nl-primary-color);
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Public API
|
||
static init(options) {
|
||
if (Modal.instance) return Modal.instance;
|
||
Modal.instance = new Modal(options);
|
||
return Modal.instance;
|
||
}
|
||
|
||
static getInstance() {
|
||
return Modal.instance;
|
||
}
|
||
}
|
||
|
||
// Initialize global instance
|
||
let modalInstance = null;
|
||
|
||
window.addEventListener('load', () => {
|
||
modalInstance = new Modal();
|
||
});
|
||
|
||
|
||
// ======================================
|
||
// FloatingTab Component (Recovered from git history)
|
||
// ======================================
|
||
|
||
class FloatingTab {
|
||
constructor(modal, options = {}) {
|
||
this.modal = modal;
|
||
this.options = {
|
||
enabled: true,
|
||
hPosition: 1.0, // 0.0 = left, 1.0 = right
|
||
vPosition: 0.5, // 0.0 = top, 1.0 = bottom
|
||
offset: { x: 0, y: 0 },
|
||
appearance: {
|
||
style: 'pill', // 'pill', 'square', 'circle'
|
||
theme: 'auto', // 'auto', 'light', 'dark'
|
||
icon: '',
|
||
text: 'Login',
|
||
iconOnly: false
|
||
},
|
||
behavior: {
|
||
hideWhenAuthenticated: true,
|
||
showUserInfo: true,
|
||
autoSlide: true,
|
||
persistent: false
|
||
},
|
||
getUserInfo: false,
|
||
getUserRelay: [],
|
||
...options
|
||
};
|
||
|
||
this.userProfile = null;
|
||
this.container = null;
|
||
this.isVisible = false;
|
||
|
||
if (this.options.enabled) {
|
||
this._init();
|
||
}
|
||
}
|
||
|
||
_init() {
|
||
console.log('FloatingTab: Initializing with options:', this.options);
|
||
this._createContainer();
|
||
this._setupEventListeners();
|
||
this._updateAppearance();
|
||
this._position();
|
||
this.show();
|
||
}
|
||
|
||
// Get authentication state from authoritative source (Global Storage-Based Function)
|
||
_getAuthState() {
|
||
return window.NOSTR_LOGIN_LITE?.getAuthState?.() || null;
|
||
}
|
||
|
||
|
||
_createContainer() {
|
||
// Remove existing floating tab if any
|
||
const existingTab = document.getElementById('nl-floating-tab');
|
||
if (existingTab) {
|
||
existingTab.remove();
|
||
}
|
||
|
||
this.container = document.createElement('div');
|
||
this.container.id = 'nl-floating-tab';
|
||
this.container.className = 'nl-floating-tab';
|
||
|
||
// Base styles - positioning and behavior
|
||
this.container.style.cssText = `
|
||
position: fixed;
|
||
z-index: 9999;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
padding: 8px 16px;
|
||
min-width: 80px;
|
||
max-width: 200px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
`;
|
||
|
||
document.body.appendChild(this.container);
|
||
}
|
||
|
||
_setupEventListeners() {
|
||
if (!this.container) return;
|
||
|
||
// Click handler
|
||
this.container.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this._handleClick();
|
||
});
|
||
|
||
// Hover effects
|
||
this.container.addEventListener('mouseenter', () => {
|
||
if (this.options.behavior.autoSlide) {
|
||
this._slideIn();
|
||
}
|
||
});
|
||
|
||
this.container.addEventListener('mouseleave', () => {
|
||
if (this.options.behavior.autoSlide) {
|
||
this._slideOut();
|
||
}
|
||
});
|
||
|
||
// Listen for authentication events
|
||
window.addEventListener('nlMethodSelected', (e) => {
|
||
console.log('🔍 FloatingTab: Authentication method selected event received');
|
||
console.log('🔍 FloatingTab: Event detail:', e.detail);
|
||
this._handleAuth(e.detail);
|
||
});
|
||
|
||
window.addEventListener('nlAuthRestored', (e) => {
|
||
console.log('🔍 FloatingTab: ✅ Authentication restored event received');
|
||
console.log('🔍 FloatingTab: Event detail:', e.detail);
|
||
console.log('🔍 FloatingTab: Calling _handleAuth with restored data...');
|
||
this._handleAuth(e.detail);
|
||
});
|
||
|
||
window.addEventListener('nlLogout', () => {
|
||
console.log('🔍 FloatingTab: Logout event received');
|
||
this._handleLogout();
|
||
});
|
||
|
||
// Check for existing authentication state on initialization
|
||
window.addEventListener('load', () => {
|
||
setTimeout(() => {
|
||
this._checkExistingAuth();
|
||
}, 1000); // Wait 1 second for all initialization to complete
|
||
});
|
||
}
|
||
|
||
// Check for existing authentication on page load
|
||
async _checkExistingAuth() {
|
||
console.log('🔍 FloatingTab: === _checkExistingAuth START ===');
|
||
|
||
try {
|
||
const storageKey = 'nostr_login_lite_auth';
|
||
let storedAuth = null;
|
||
|
||
// Try sessionStorage first, then localStorage
|
||
if (sessionStorage.getItem(storageKey)) {
|
||
storedAuth = JSON.parse(sessionStorage.getItem(storageKey));
|
||
console.log('🔍 FloatingTab: Found auth in sessionStorage:', storedAuth.method);
|
||
} else if (localStorage.getItem(storageKey)) {
|
||
storedAuth = JSON.parse(localStorage.getItem(storageKey));
|
||
console.log('🔍 FloatingTab: Found auth in localStorage:', storedAuth.method);
|
||
}
|
||
|
||
if (storedAuth) {
|
||
// Check if stored auth is not expired
|
||
const maxAge = storedAuth.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
|
||
if (Date.now() - storedAuth.timestamp <= maxAge) {
|
||
console.log('🔍 FloatingTab: Found valid stored auth, simulating auth event');
|
||
|
||
// Create auth data object for FloatingTab
|
||
const authData = {
|
||
method: storedAuth.method,
|
||
pubkey: storedAuth.pubkey
|
||
};
|
||
|
||
// For extensions, try to find the extension
|
||
if (storedAuth.method === 'extension') {
|
||
if (window.nostr && window.nostr.constructor?.name !== 'WindowNostr') {
|
||
authData.extension = window.nostr;
|
||
}
|
||
}
|
||
|
||
await this._handleAuth(authData);
|
||
} else {
|
||
console.log('🔍 FloatingTab: Stored auth expired, clearing');
|
||
sessionStorage.removeItem(storageKey);
|
||
localStorage.removeItem(storageKey);
|
||
}
|
||
} else {
|
||
console.log('🔍 FloatingTab: No existing authentication found');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('🔍 FloatingTab: Error checking existing auth:', error);
|
||
}
|
||
|
||
console.log('🔍 FloatingTab: === _checkExistingAuth END ===');
|
||
}
|
||
|
||
_handleClick() {
|
||
console.log('FloatingTab: Clicked');
|
||
|
||
const authState = this._getAuthState();
|
||
if (authState && this.options.behavior.showUserInfo) {
|
||
// Show user menu or profile options
|
||
this._showUserMenu();
|
||
} else {
|
||
// Always open login modal (consistent with login buttons)
|
||
if (this.modal) {
|
||
this.modal.open({ startScreen: 'login' });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check if object is a real extension (same logic as NostrLite._isRealExtension)
|
||
_isRealExtension(obj) {
|
||
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 classes
|
||
const constructorName = obj.constructor?.name;
|
||
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
|
||
return false;
|
||
}
|
||
|
||
// Exclude NostrTools library object
|
||
if (obj === window.NostrTools) {
|
||
return false;
|
||
}
|
||
|
||
// Conservative check: Look for common extension characteristics
|
||
const extensionIndicators = [
|
||
'_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope',
|
||
'_requests', '_pubkey', 'name', 'version', 'description'
|
||
];
|
||
|
||
const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop));
|
||
|
||
// Additional check: Extensions often have specific constructor patterns
|
||
const hasExtensionConstructor = constructorName &&
|
||
constructorName !== 'Object' &&
|
||
constructorName !== 'Function';
|
||
|
||
return hasIndicators || hasExtensionConstructor;
|
||
}
|
||
|
||
// Try to login with extension and trigger proper persistence
|
||
async _tryExtensionLogin(extension) {
|
||
try {
|
||
console.log('FloatingTab: Attempting extension login');
|
||
|
||
// Get pubkey from extension
|
||
const pubkey = await extension.getPublicKey();
|
||
console.log('FloatingTab: Extension provided pubkey:', pubkey);
|
||
|
||
// Create extension auth data
|
||
const extensionAuth = {
|
||
method: 'extension',
|
||
pubkey: pubkey,
|
||
extension: extension
|
||
};
|
||
|
||
// **CRITICAL FIX**: Dispatch nlMethodSelected event to trigger persistence
|
||
console.log('FloatingTab: Dispatching nlMethodSelected for persistence');
|
||
if (typeof window !== 'undefined') {
|
||
window.dispatchEvent(new CustomEvent('nlMethodSelected', {
|
||
detail: extensionAuth
|
||
}));
|
||
}
|
||
|
||
// Also call our local _handleAuth for UI updates
|
||
await this._handleAuth(extensionAuth);
|
||
|
||
} catch (error) {
|
||
console.error('FloatingTab: Extension login failed:', error);
|
||
// Fall back to opening modal
|
||
if (this.modal) {
|
||
this.modal.open({ startScreen: 'login' });
|
||
}
|
||
}
|
||
}
|
||
|
||
async _handleAuth(authData) {
|
||
console.log('🔍 FloatingTab: === _handleAuth START ===');
|
||
console.log('🔍 FloatingTab: authData received:', authData);
|
||
|
||
// Wait a brief moment for WindowNostr to process the authentication
|
||
setTimeout(async () => {
|
||
console.log('🔍 FloatingTab: Checking authentication state from authoritative source...');
|
||
|
||
const authState = this._getAuthState();
|
||
const isAuthenticated = !!authState;
|
||
|
||
console.log('🔍 FloatingTab: Authoritative auth state:', authState);
|
||
console.log('🔍 FloatingTab: Is authenticated:', isAuthenticated);
|
||
|
||
if (isAuthenticated) {
|
||
console.log('🔍 FloatingTab: ✅ Authentication verified from authoritative source');
|
||
} else {
|
||
console.error('🔍 FloatingTab: ❌ Authentication not found in authoritative source');
|
||
}
|
||
|
||
// Fetch user profile if enabled and we have a pubkey
|
||
if (this.options.getUserInfo && authData.pubkey) {
|
||
console.log('🔍 FloatingTab: getUserInfo enabled, fetching profile for:', authData.pubkey);
|
||
try {
|
||
const profile = await this._fetchUserProfile(authData.pubkey);
|
||
this.userProfile = profile;
|
||
console.log('🔍 FloatingTab: User profile fetched:', profile);
|
||
} catch (error) {
|
||
console.warn('🔍 FloatingTab: Failed to fetch user profile:', error);
|
||
this.userProfile = null;
|
||
}
|
||
} else {
|
||
console.log('🔍 FloatingTab: getUserInfo disabled or no pubkey, skipping profile fetch');
|
||
}
|
||
|
||
this._updateAppearance(); // Update UI based on authoritative state
|
||
|
||
console.log('🔍 FloatingTab: hideWhenAuthenticated option:', this.options.behavior.hideWhenAuthenticated);
|
||
|
||
if (this.options.behavior.hideWhenAuthenticated && isAuthenticated) {
|
||
console.log('🔍 FloatingTab: Hiding tab (hideWhenAuthenticated=true and authenticated)');
|
||
this.hide();
|
||
} else {
|
||
console.log('🔍 FloatingTab: Keeping tab visible');
|
||
}
|
||
|
||
}, 500); // Wait 500ms for WindowNostr to complete authentication processing
|
||
|
||
console.log('🔍 FloatingTab: === _handleAuth END ===');
|
||
}
|
||
|
||
_handleLogout() {
|
||
console.log('FloatingTab: Handling logout');
|
||
this.userProfile = null;
|
||
|
||
if (this.options.behavior.hideWhenAuthenticated) {
|
||
this.show();
|
||
}
|
||
|
||
this._updateAppearance();
|
||
}
|
||
|
||
_showUserMenu() {
|
||
// Simple user menu - could be expanded
|
||
const menu = document.createElement('div');
|
||
menu.style.cssText = `
|
||
position: fixed;
|
||
background: var(--nl-secondary-color);
|
||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||
border-radius: var(--nl-border-radius);
|
||
padding: 12px;
|
||
z-index: 10000;
|
||
font-family: var(--nl-font-family);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
`;
|
||
|
||
// Position near the floating tab
|
||
const tabRect = this.container.getBoundingClientRect();
|
||
if (this.options.hPosition > 0.5) {
|
||
// Tab is on right side, show menu to the left
|
||
menu.style.right = (window.innerWidth - tabRect.left) + 'px';
|
||
} else {
|
||
// Tab is on left side, show menu to the right
|
||
menu.style.left = tabRect.right + 'px';
|
||
}
|
||
menu.style.top = tabRect.top + 'px';
|
||
|
||
// Menu content - use _getAuthState() as single source of truth
|
||
const authState = this._getAuthState();
|
||
let userDisplay;
|
||
|
||
if (authState?.pubkey) {
|
||
// Use profile name if available, otherwise pubkey
|
||
if (this.userProfile?.name || this.userProfile?.display_name) {
|
||
const userName = this.userProfile.name || this.userProfile.display_name;
|
||
userDisplay = userName.length > 16 ? `${userName.slice(0, 16)}...` : userName;
|
||
} else {
|
||
userDisplay = `${authState.pubkey.slice(0, 8)}...${authState.pubkey.slice(-4)}`;
|
||
}
|
||
} else {
|
||
userDisplay = 'Authenticated';
|
||
}
|
||
|
||
menu.innerHTML = `
|
||
<div style="margin-bottom: 8px; font-weight: bold; color: var(--nl-primary-color);">${userDisplay}</div>
|
||
<button onclick="window.NOSTR_LOGIN_LITE.logout(); this.parentElement.remove();"
|
||
style="background: var(--nl-secondary-color); color: var(--nl-primary-color);
|
||
border: 1px solid var(--nl-primary-color); border-radius: 4px;
|
||
padding: 6px 12px; cursor: pointer; width: 100%;">
|
||
Logout
|
||
</button>
|
||
`;
|
||
|
||
document.body.appendChild(menu);
|
||
|
||
// Auto-remove menu after delay or on outside click
|
||
const removeMenu = () => menu.remove();
|
||
setTimeout(removeMenu, 5000);
|
||
|
||
document.addEventListener('click', function onOutsideClick(e) {
|
||
if (!menu.contains(e.target) && e.target !== this.container) {
|
||
removeMenu();
|
||
document.removeEventListener('click', onOutsideClick);
|
||
}
|
||
});
|
||
}
|
||
|
||
_updateAppearance() {
|
||
if (!this.container) return;
|
||
|
||
// Query authoritative source for all state information
|
||
const authState = this._getAuthState();
|
||
const isAuthenticated = authState !== null;
|
||
|
||
// Update content
|
||
if (isAuthenticated && this.options.behavior.showUserInfo) {
|
||
let display;
|
||
|
||
// Use profile name if available, otherwise fall back to pubkey
|
||
if (this.userProfile?.name || this.userProfile?.display_name) {
|
||
const userName = this.userProfile.name || this.userProfile.display_name;
|
||
display = this.options.appearance.iconOnly
|
||
? userName.slice(0, 8)
|
||
: userName;
|
||
} else if (authState?.pubkey) {
|
||
// Fallback to pubkey display
|
||
display = this.options.appearance.iconOnly
|
||
? authState.pubkey.slice(0, 6)
|
||
: `${authState.pubkey.slice(0, 6)}...`;
|
||
} else {
|
||
display = this.options.appearance.iconOnly ? 'User' : 'Authenticated';
|
||
}
|
||
|
||
this.container.textContent = display;
|
||
this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
|
||
} else {
|
||
const display = this.options.appearance.iconOnly ?
|
||
this.options.appearance.icon :
|
||
(this.options.appearance.icon ? `${this.options.appearance.icon} ${this.options.appearance.text}` : this.options.appearance.text);
|
||
|
||
this.container.textContent = display;
|
||
this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
|
||
}
|
||
|
||
// Apply appearance styles based on current state
|
||
this._applyThemeStyles();
|
||
}
|
||
|
||
_applyThemeStyles() {
|
||
if (!this.container) return;
|
||
|
||
// The CSS classes will handle the theming through CSS custom properties
|
||
// Additional style customizations can be added here if needed
|
||
|
||
// Apply style variant
|
||
if (this.options.appearance.style === 'circle') {
|
||
this.container.style.borderRadius = '50%';
|
||
this.container.style.width = '48px';
|
||
this.container.style.height = '48px';
|
||
this.container.style.minWidth = '48px';
|
||
this.container.style.padding = '0';
|
||
} else if (this.options.appearance.style === 'square') {
|
||
this.container.style.borderRadius = '4px';
|
||
} else {
|
||
// pill style (default)
|
||
this.container.style.borderRadius = 'var(--nl-border-radius)';
|
||
}
|
||
}
|
||
|
||
async _fetchUserProfile(pubkey) {
|
||
if (!this.options.getUserInfo) {
|
||
console.log('FloatingTab: getUserInfo disabled, skipping profile fetch');
|
||
return null;
|
||
}
|
||
|
||
// Determine which relays to use
|
||
const relays = this.options.getUserRelay.length > 0
|
||
? this.options.getUserRelay
|
||
: ['wss://relay.damus.io', 'wss://nos.lol'];
|
||
|
||
console.log('FloatingTab: Fetching profile from relays:', relays);
|
||
|
||
try {
|
||
// Create a SimplePool instance for querying
|
||
const pool = new window.NostrTools.SimplePool();
|
||
|
||
// Query for kind 0 (user metadata) events
|
||
const events = await pool.querySync(relays, {
|
||
kinds: [0],
|
||
authors: [pubkey],
|
||
limit: 1
|
||
}, { timeout: 5000 });
|
||
|
||
console.log('FloatingTab: Profile query returned', events.length, 'events');
|
||
|
||
if (events.length === 0) {
|
||
console.log('FloatingTab: No profile events found');
|
||
return null;
|
||
}
|
||
|
||
// Get the most recent event
|
||
const latestEvent = events.sort((a, b) => b.created_at - a.created_at)[0];
|
||
|
||
try {
|
||
const profile = JSON.parse(latestEvent.content);
|
||
console.log('FloatingTab: Parsed profile:', profile);
|
||
|
||
// Find the best name from any key containing "name" (case-insensitive)
|
||
let bestName = null;
|
||
const nameKeys = Object.keys(profile).filter(key =>
|
||
key.toLowerCase().includes('name') &&
|
||
typeof profile[key] === 'string' &&
|
||
profile[key].trim().length > 0
|
||
);
|
||
|
||
if (nameKeys.length > 0) {
|
||
// Find the shortest name value
|
||
bestName = nameKeys
|
||
.map(key => profile[key].trim())
|
||
.reduce((shortest, current) =>
|
||
current.length < shortest.length ? current : shortest
|
||
);
|
||
console.log('FloatingTab: Found name keys:', nameKeys, 'selected:', bestName);
|
||
}
|
||
|
||
// Return relevant profile fields with the best name
|
||
return {
|
||
name: bestName,
|
||
display_name: profile.display_name || null,
|
||
about: profile.about || null,
|
||
picture: profile.picture || null,
|
||
nip05: profile.nip05 || null
|
||
};
|
||
} catch (parseError) {
|
||
console.warn('FloatingTab: Failed to parse profile JSON:', parseError);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error('FloatingTab: Profile fetch error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
_position() {
|
||
if (!this.container) return;
|
||
|
||
const padding = 16; // Distance from screen edge
|
||
|
||
// Calculate position based on percentage
|
||
const x = this.options.hPosition * (window.innerWidth - this.container.offsetWidth - padding * 2) + padding + this.options.offset.x;
|
||
const y = this.options.vPosition * (window.innerHeight - this.container.offsetHeight - padding * 2) + padding + this.options.offset.y;
|
||
|
||
this.container.style.left = `${x}px`;
|
||
this.container.style.top = `${y}px`;
|
||
|
||
console.log(`FloatingTab: Positioned at (${x}, ${y})`);
|
||
}
|
||
|
||
_slideIn() {
|
||
if (!this.container || !this.options.behavior.autoSlide) return;
|
||
|
||
// Slide towards center slightly
|
||
const currentTransform = this.container.style.transform || '';
|
||
if (this.options.hPosition > 0.5) {
|
||
this.container.style.transform = currentTransform + ' translateX(-8px)';
|
||
} else {
|
||
this.container.style.transform = currentTransform + ' translateX(8px)';
|
||
}
|
||
}
|
||
|
||
_slideOut() {
|
||
if (!this.container || !this.options.behavior.autoSlide) return;
|
||
|
||
// Reset position
|
||
this.container.style.transform = '';
|
||
}
|
||
|
||
show() {
|
||
if (!this.container) return;
|
||
this.container.style.display = 'flex';
|
||
this.isVisible = true;
|
||
console.log('FloatingTab: Shown');
|
||
}
|
||
|
||
hide() {
|
||
if (!this.container) return;
|
||
this.container.style.display = 'none';
|
||
this.isVisible = false;
|
||
console.log('FloatingTab: Hidden');
|
||
}
|
||
|
||
destroy() {
|
||
if (this.container) {
|
||
this.container.remove();
|
||
this.container = null;
|
||
}
|
||
this.isVisible = false;
|
||
console.log('FloatingTab: Destroyed');
|
||
}
|
||
|
||
// Update options and re-apply
|
||
updateOptions(newOptions) {
|
||
this.options = { ...this.options, ...newOptions };
|
||
if (this.container) {
|
||
this._updateAppearance();
|
||
this._position();
|
||
}
|
||
}
|
||
|
||
// Get current state
|
||
getState() {
|
||
const authState = this._getAuthState();
|
||
return {
|
||
isVisible: this.isVisible,
|
||
isAuthenticated: !!authState,
|
||
userInfo: authState,
|
||
options: this.options
|
||
};
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// Main NOSTR_LOGIN_LITE Library
|
||
// ======================================
|
||
|
||
// Extension Bridge for managing browser extensions
|
||
class ExtensionBridge {
|
||
constructor() {
|
||
this.extensions = new Map();
|
||
this.primaryExtension = null;
|
||
this._detectExtensions();
|
||
}
|
||
|
||
_detectExtensions() {
|
||
// Common extension locations
|
||
const locations = [
|
||
{ path: 'window.nostr', name: 'Generic' },
|
||
{ path: 'window.alby?.nostr', name: 'Alby' },
|
||
{ path: 'window.nos2x?.nostr', name: 'nos2x' },
|
||
{ path: 'window.flamingo?.nostr', name: 'Flamingo' },
|
||
{ path: 'window.getAlby?.nostr', name: 'Alby Legacy' },
|
||
{ path: 'window.mutiny?.nostr', name: 'Mutiny' }
|
||
];
|
||
|
||
for (const location of locations) {
|
||
try {
|
||
const obj = eval(location.path);
|
||
if (obj && typeof obj.getPublicKey === 'function') {
|
||
this.extensions.set(location.name, {
|
||
name: location.name,
|
||
extension: obj,
|
||
constructor: obj.constructor?.name || 'Unknown'
|
||
});
|
||
|
||
if (!this.primaryExtension) {
|
||
this.primaryExtension = this.extensions.get(location.name);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Extension not available
|
||
}
|
||
}
|
||
}
|
||
|
||
getAllExtensions() {
|
||
return Array.from(this.extensions.values());
|
||
}
|
||
|
||
getExtensionCount() {
|
||
return this.extensions.size;
|
||
}
|
||
}
|
||
|
||
// Main NostrLite class
|
||
class NostrLite {
|
||
constructor() {
|
||
this.options = {};
|
||
this.extensionBridge = new ExtensionBridge();
|
||
this.initialized = false;
|
||
this.currentTheme = 'default';
|
||
this.modal = null;
|
||
this.floatingTab = null;
|
||
}
|
||
|
||
async init(options = {}) {
|
||
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
|
||
|
||
this.options = {
|
||
theme: 'default',
|
||
persistence: true, // Enable persistent authentication by default
|
||
isolateSession: false, // Use localStorage by default for cross-window persistence
|
||
methods: {
|
||
extension: true,
|
||
local: true,
|
||
seedphrase: false,
|
||
readonly: true,
|
||
connect: false,
|
||
otp: false
|
||
},
|
||
floatingTab: {
|
||
enabled: false,
|
||
hPosition: 1.0,
|
||
vPosition: 0.5,
|
||
offset: { x: 0, y: 0 },
|
||
appearance: {
|
||
style: 'pill',
|
||
theme: 'auto',
|
||
icon: '',
|
||
text: 'Login',
|
||
iconOnly: false
|
||
},
|
||
behavior: {
|
||
hideWhenAuthenticated: true,
|
||
showUserInfo: true,
|
||
autoSlide: true,
|
||
persistent: false
|
||
},
|
||
getUserInfo: false,
|
||
getUserRelay: []
|
||
},
|
||
...options
|
||
};
|
||
|
||
// Apply the selected theme (CSS-only)
|
||
this.switchTheme(this.options.theme);
|
||
|
||
// Always set up window.nostr facade to handle multiple extensions properly
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Setting up facade before other initialization...');
|
||
await this._setupWindowNostrFacade();
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Facade setup complete, continuing initialization...');
|
||
|
||
// Create modal during init (matching original git architecture)
|
||
this.modal = new Modal(this.options);
|
||
console.log('NOSTR_LOGIN_LITE: Modal created during init');
|
||
|
||
// Initialize floating tab if enabled
|
||
if (this.options.floatingTab.enabled) {
|
||
this.floatingTab = new FloatingTab(this.modal, this.options.floatingTab);
|
||
console.log('NOSTR_LOGIN_LITE: Floating tab initialized');
|
||
}
|
||
|
||
// Attempt to restore authentication state if persistence is enabled (AFTER facade is ready)
|
||
if (this.options.persistence) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Persistence enabled, attempting auth restoration...');
|
||
await this._attemptAuthRestore();
|
||
} else {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Persistence disabled in options');
|
||
}
|
||
|
||
this.initialized = true;
|
||
console.log('NOSTR_LOGIN_LITE: Initialization complete');
|
||
|
||
return this;
|
||
}
|
||
|
||
async _setupWindowNostrFacade() {
|
||
if (typeof window !== 'undefined') {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: === EXTENSION-FIRST FACADE SETUP ===');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Current window.nostr:', window.nostr);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Constructor:', window.nostr?.constructor?.name);
|
||
|
||
// EXTENSION-FIRST ARCHITECTURE: Never interfere with real extensions
|
||
if (this._isRealExtension(window.nostr)) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ✅ REAL EXTENSION DETECTED - WILL NOT INSTALL FACADE');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Extension constructor:', window.nostr.constructor?.name);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Extensions will handle window.nostr directly');
|
||
|
||
// Store reference for persistence verification
|
||
this.detectedExtension = window.nostr;
|
||
this.hasExtension = true;
|
||
this.facadeInstalled = false; // We deliberately don't install facade for extensions
|
||
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - no facade interference');
|
||
return; // Don't install facade at all for extensions
|
||
}
|
||
|
||
// NO EXTENSION: Install facade for local/NIP-46/readonly methods
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ❌ No real extension detected');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Installing facade for non-extension authentication');
|
||
|
||
this.hasExtension = false;
|
||
this._installFacade(window.nostr); // Install facade with any existing nostr object
|
||
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade installed for local/NIP-46/readonly methods');
|
||
}
|
||
}
|
||
|
||
_installFacade(existingNostr = null) {
|
||
if (typeof window !== 'undefined' && !this.facadeInstalled) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: === _installFacade CALLED ===');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name);
|
||
|
||
const facade = new WindowNostr(this, existingNostr, { isolateSession: this.options.isolateSession });
|
||
window.nostr = facade;
|
||
this.facadeInstalled = true;
|
||
|
||
console.log('🔍 NOSTR_LOGIN_LITE: === FACADE INSTALLED FOR PERSISTENCE ===');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr);
|
||
}
|
||
}
|
||
|
||
// Conservative method to identify real browser extensions
|
||
_isRealExtension(obj) {
|
||
console.log('NOSTR_LOGIN_LITE: === _isRealExtension (Conservative) ===');
|
||
console.log('NOSTR_LOGIN_LITE: obj:', obj);
|
||
console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj);
|
||
|
||
if (!obj || typeof obj !== 'object') {
|
||
console.log('NOSTR_LOGIN_LITE: ✗ Not an object');
|
||
return false;
|
||
}
|
||
|
||
// Must have required Nostr methods
|
||
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') {
|
||
console.log('NOSTR_LOGIN_LITE: ✗ Missing required NIP-07 methods');
|
||
return false;
|
||
}
|
||
|
||
// Exclude our own library classes
|
||
const constructorName = obj.constructor?.name;
|
||
console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName);
|
||
|
||
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
|
||
console.log('NOSTR_LOGIN_LITE: ✗ Is our library class - NOT an extension');
|
||
return false;
|
||
}
|
||
|
||
// Exclude NostrTools library object
|
||
if (obj === window.NostrTools) {
|
||
console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object - NOT an extension');
|
||
return false;
|
||
}
|
||
|
||
// Conservative check: Look for common extension characteristics
|
||
// Real extensions usually have some of these internal properties
|
||
const extensionIndicators = [
|
||
'_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope',
|
||
'_requests', '_pubkey', 'name', 'version', 'description'
|
||
];
|
||
|
||
const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop));
|
||
|
||
// Additional check: Extensions often have specific constructor patterns
|
||
const hasExtensionConstructor = constructorName &&
|
||
constructorName !== 'Object' &&
|
||
constructorName !== 'Function';
|
||
|
||
const isExtension = hasIndicators || hasExtensionConstructor;
|
||
|
||
console.log('NOSTR_LOGIN_LITE: Extension indicators found:', hasIndicators);
|
||
console.log('NOSTR_LOGIN_LITE: Has extension constructor:', hasExtensionConstructor);
|
||
console.log('NOSTR_LOGIN_LITE: Final result for', constructorName, ':', isExtension);
|
||
|
||
return isExtension;
|
||
}
|
||
|
||
launch(startScreen = 'login') {
|
||
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
||
|
||
if (this.modal) {
|
||
this.modal.open({ startScreen });
|
||
} else {
|
||
console.error('NOSTR_LOGIN_LITE: Modal not initialized - call init() first');
|
||
}
|
||
}
|
||
|
||
// Attempt to restore authentication state
|
||
async _attemptAuthRestore() {
|
||
try {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: === _attemptAuthRestore START ===');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr:', window.nostr?.constructor?.name);
|
||
|
||
if (this.hasExtension) {
|
||
// EXTENSION MODE: Use custom extension persistence logic
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Extension mode - using extension-specific restore');
|
||
const restoredAuth = await this._attemptExtensionRestore();
|
||
|
||
if (restoredAuth) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth restored successfully!');
|
||
return restoredAuth;
|
||
} else {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ❌ Extension auth could not be restored');
|
||
return null;
|
||
}
|
||
} else if (this.facadeInstalled && window.nostr?.restoreAuthState) {
|
||
// NON-EXTENSION MODE: Use facade persistence logic
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Non-extension mode - using facade restore');
|
||
const restoredAuth = await window.nostr.restoreAuthState();
|
||
|
||
if (restoredAuth) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Facade auth restored successfully!');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Method:', restoredAuth.method);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Pubkey:', restoredAuth.pubkey);
|
||
|
||
// Handle NIP-46 reconnection requirement
|
||
if (restoredAuth.requiresReconnection) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: NIP-46 connection requires user reconnection');
|
||
this._showReconnectionPrompt(restoredAuth);
|
||
}
|
||
|
||
return restoredAuth;
|
||
} else {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ❌ Facade auth could not be restored');
|
||
return null;
|
||
}
|
||
} else {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ❌ No restoration method available');
|
||
console.log('🔍 NOSTR_LOGIN_LITE: hasExtension:', this.hasExtension);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: facadeInstalled:', this.facadeInstalled);
|
||
console.log('🔍 NOSTR_LOGIN_LITE: window.nostr.restoreAuthState:', typeof window.nostr?.restoreAuthState);
|
||
return null;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('🔍 NOSTR_LOGIN_LITE: Auth restoration failed with error:', error);
|
||
console.error('🔍 NOSTR_LOGIN_LITE: Error stack:', error.stack);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Extension-specific authentication restoration
|
||
async _attemptExtensionRestore() {
|
||
try {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: === _attemptExtensionRestore START ===');
|
||
|
||
// Use a simple AuthManager instance for extension persistence
|
||
const authManager = new AuthManager({ isolateSession: this.options?.isolateSession });
|
||
const storedAuth = await authManager.restoreAuthState();
|
||
|
||
if (!storedAuth || storedAuth.method !== 'extension') {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: No extension auth state stored');
|
||
return null;
|
||
}
|
||
|
||
// Verify the extension is still available and working
|
||
if (!window.nostr || !this._isRealExtension(window.nostr)) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Extension no longer available');
|
||
authManager.clearAuthState(); // Clear invalid state
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
// Test that the extension still works with the same pubkey
|
||
const currentPubkey = await window.nostr.getPublicKey();
|
||
if (currentPubkey !== storedAuth.pubkey) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Extension pubkey changed, clearing state');
|
||
authManager.clearAuthState();
|
||
return null;
|
||
}
|
||
|
||
console.log('🔍 NOSTR_LOGIN_LITE: ✅ Extension auth verification successful');
|
||
|
||
// Create extension auth data for UI restoration
|
||
const extensionAuth = {
|
||
method: 'extension',
|
||
pubkey: storedAuth.pubkey,
|
||
extension: window.nostr
|
||
};
|
||
|
||
// Dispatch restoration event so UI can update
|
||
if (typeof window !== 'undefined') {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Dispatching nlAuthRestored event for extension');
|
||
window.dispatchEvent(new CustomEvent('nlAuthRestored', {
|
||
detail: extensionAuth
|
||
}));
|
||
}
|
||
|
||
return extensionAuth;
|
||
|
||
} catch (error) {
|
||
console.log('🔍 NOSTR_LOGIN_LITE: Extension verification failed:', error);
|
||
authManager.clearAuthState(); // Clear invalid state
|
||
return null;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('🔍 NOSTR_LOGIN_LITE: Extension restore failed:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Show prompt for NIP-46 reconnection
|
||
_showReconnectionPrompt(authData) {
|
||
console.log('NOSTR_LOGIN_LITE: Showing reconnection prompt for NIP-46');
|
||
|
||
// Dispatch event that UI can listen to
|
||
if (typeof window !== 'undefined') {
|
||
window.dispatchEvent(new CustomEvent('nlReconnectionRequired', {
|
||
detail: {
|
||
method: authData.method,
|
||
pubkey: authData.pubkey,
|
||
connectionData: authData.connectionData,
|
||
message: 'Your NIP-46 session has expired. Please reconnect to continue.'
|
||
}
|
||
}));
|
||
}
|
||
}
|
||
|
||
logout() {
|
||
console.log('NOSTR_LOGIN_LITE: Logout called');
|
||
|
||
// Clear legacy stored data
|
||
if (typeof localStorage !== 'undefined') {
|
||
localStorage.removeItem('nl_current');
|
||
}
|
||
|
||
// Clear current authentication state directly from storage
|
||
// This works for ALL methods including extensions (fixes the bug)
|
||
clearAuthState();
|
||
|
||
// Dispatch logout event for UI updates
|
||
if (typeof window !== 'undefined') {
|
||
window.dispatchEvent(new CustomEvent('nlLogout', {
|
||
detail: { timestamp: Date.now() }
|
||
}));
|
||
}
|
||
}
|
||
|
||
// CSS-only theme switching
|
||
switchTheme(themeName) {
|
||
console.log(`NOSTR_LOGIN_LITE: Switching to ${themeName} theme`);
|
||
|
||
if (THEME_CSS[themeName]) {
|
||
injectThemeCSS(themeName);
|
||
this.currentTheme = themeName;
|
||
|
||
// Dispatch theme change event
|
||
if (typeof window !== 'undefined') {
|
||
window.dispatchEvent(new CustomEvent('nlThemeChanged', {
|
||
detail: { theme: themeName }
|
||
}));
|
||
}
|
||
|
||
return { theme: themeName };
|
||
} else {
|
||
console.warn(`Theme '${themeName}' not found, using default`);
|
||
injectThemeCSS('default');
|
||
this.currentTheme = 'default';
|
||
return { theme: 'default' };
|
||
}
|
||
}
|
||
|
||
getCurrentTheme() {
|
||
return this.currentTheme;
|
||
}
|
||
|
||
getAvailableThemes() {
|
||
return Object.keys(THEME_CSS);
|
||
}
|
||
|
||
embed(container, options = {}) {
|
||
console.log('NOSTR_LOGIN_LITE: Creating embedded modal in container:', container);
|
||
|
||
const embedOptions = {
|
||
...this.options,
|
||
...options,
|
||
embedded: container
|
||
};
|
||
|
||
// Create new modal instance for embedding
|
||
const embeddedModal = new Modal(embedOptions);
|
||
embeddedModal.open();
|
||
|
||
return embeddedModal;
|
||
}
|
||
|
||
// Floating tab management methods
|
||
showFloatingTab() {
|
||
if (this.floatingTab) {
|
||
this.floatingTab.show();
|
||
} else {
|
||
console.warn('NOSTR_LOGIN_LITE: Floating tab not enabled');
|
||
}
|
||
}
|
||
|
||
hideFloatingTab() {
|
||
if (this.floatingTab) {
|
||
this.floatingTab.hide();
|
||
}
|
||
}
|
||
|
||
toggleFloatingTab() {
|
||
if (this.floatingTab) {
|
||
if (this.floatingTab.isVisible) {
|
||
this.floatingTab.hide();
|
||
} else {
|
||
this.floatingTab.show();
|
||
}
|
||
}
|
||
}
|
||
|
||
updateFloatingTab(options) {
|
||
if (this.floatingTab) {
|
||
this.floatingTab.updateOptions(options);
|
||
}
|
||
}
|
||
|
||
getFloatingTabState() {
|
||
return this.floatingTab ? this.floatingTab.getState() : null;
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// Authentication Manager for Persistent Login
|
||
// ======================================
|
||
|
||
// Encryption utilities for secure local storage
|
||
class CryptoUtils {
|
||
static async generateKey() {
|
||
if (!window.crypto?.subtle) {
|
||
throw new Error('Web Crypto API not available');
|
||
}
|
||
|
||
return await window.crypto.subtle.generateKey(
|
||
{
|
||
name: 'AES-GCM',
|
||
length: 256,
|
||
},
|
||
true,
|
||
['encrypt', 'decrypt']
|
||
);
|
||
}
|
||
|
||
static async deriveKey(password, salt) {
|
||
if (!window.crypto?.subtle) {
|
||
throw new Error('Web Crypto API not available');
|
||
}
|
||
|
||
const encoder = new TextEncoder();
|
||
const keyMaterial = await window.crypto.subtle.importKey(
|
||
'raw',
|
||
encoder.encode(password),
|
||
{ name: 'PBKDF2' },
|
||
false,
|
||
['deriveBits', 'deriveKey']
|
||
);
|
||
|
||
return await window.crypto.subtle.deriveKey(
|
||
{
|
||
name: 'PBKDF2',
|
||
salt: salt,
|
||
iterations: 100000,
|
||
hash: 'SHA-256',
|
||
},
|
||
keyMaterial,
|
||
{ name: 'AES-GCM', length: 256 },
|
||
true,
|
||
['encrypt', 'decrypt']
|
||
);
|
||
}
|
||
|
||
static async encrypt(data, key) {
|
||
if (!window.crypto?.subtle) {
|
||
throw new Error('Web Crypto API not available');
|
||
}
|
||
|
||
const encoder = new TextEncoder();
|
||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||
|
||
const encrypted = await window.crypto.subtle.encrypt(
|
||
{
|
||
name: 'AES-GCM',
|
||
iv: iv,
|
||
},
|
||
key,
|
||
encoder.encode(data)
|
||
);
|
||
|
||
return {
|
||
encrypted: new Uint8Array(encrypted),
|
||
iv: iv
|
||
};
|
||
}
|
||
|
||
static async decrypt(encryptedData, key, iv) {
|
||
if (!window.crypto?.subtle) {
|
||
throw new Error('Web Crypto API not available');
|
||
}
|
||
|
||
const decrypted = await window.crypto.subtle.decrypt(
|
||
{
|
||
name: 'AES-GCM',
|
||
iv: iv,
|
||
},
|
||
key,
|
||
encryptedData
|
||
);
|
||
|
||
const decoder = new TextDecoder();
|
||
return decoder.decode(decrypted);
|
||
}
|
||
|
||
static arrayBufferToBase64(buffer) {
|
||
const bytes = new Uint8Array(buffer);
|
||
let binary = '';
|
||
for (let i = 0; i < bytes.byteLength; i++) {
|
||
binary += String.fromCharCode(bytes[i]);
|
||
}
|
||
return window.btoa(binary);
|
||
}
|
||
|
||
static base64ToArrayBuffer(base64) {
|
||
const binary = window.atob(base64);
|
||
const bytes = new Uint8Array(binary.length);
|
||
for (let i = 0; i < binary.length; i++) {
|
||
bytes[i] = binary.charCodeAt(i);
|
||
}
|
||
return bytes.buffer;
|
||
}
|
||
}
|
||
|
||
// Unified authentication state manager
|
||
class AuthManager {
|
||
constructor(options = {}) {
|
||
this.storageKey = 'nostr_login_lite_auth';
|
||
this.currentAuthState = null;
|
||
|
||
// Configure storage type based on isolateSession option
|
||
if (options.isolateSession) {
|
||
this.storage = sessionStorage;
|
||
console.log('AuthManager: Using sessionStorage for per-window isolation');
|
||
} else {
|
||
this.storage = localStorage;
|
||
console.log('AuthManager: Using localStorage for cross-window persistence');
|
||
}
|
||
}
|
||
|
||
// Save authentication state with method-specific security
|
||
async saveAuthState(authData) {
|
||
try {
|
||
const authState = {
|
||
method: authData.method,
|
||
timestamp: Date.now(),
|
||
pubkey: authData.pubkey
|
||
};
|
||
|
||
switch (authData.method) {
|
||
case 'extension':
|
||
// For extensions, only store verification data - no secrets
|
||
authState.extensionVerification = {
|
||
constructor: authData.extension?.constructor?.name,
|
||
hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function',
|
||
hasSignEvent: typeof authData.extension?.signEvent === 'function'
|
||
};
|
||
break;
|
||
|
||
case 'local':
|
||
// For local keys, encrypt the secret key
|
||
if (authData.secret) {
|
||
const password = this._generateSessionPassword();
|
||
const salt = window.crypto.getRandomValues(new Uint8Array(16));
|
||
const key = await CryptoUtils.deriveKey(password, salt);
|
||
const encrypted = await CryptoUtils.encrypt(authData.secret, key);
|
||
|
||
authState.encrypted = {
|
||
data: CryptoUtils.arrayBufferToBase64(encrypted.encrypted),
|
||
iv: CryptoUtils.arrayBufferToBase64(encrypted.iv),
|
||
salt: CryptoUtils.arrayBufferToBase64(salt)
|
||
};
|
||
|
||
// Store session password in sessionStorage (cleared on tab close)
|
||
sessionStorage.setItem('nostr_session_key', password);
|
||
}
|
||
break;
|
||
|
||
case 'nip46':
|
||
// For NIP-46, store connection parameters (no secrets)
|
||
if (authData.signer) {
|
||
authState.nip46 = {
|
||
remotePubkey: authData.signer.remotePubkey,
|
||
relays: authData.signer.relays,
|
||
// Don't store secret - user will need to reconnect
|
||
};
|
||
}
|
||
break;
|
||
|
||
case 'readonly':
|
||
// Read-only mode has no secrets to store
|
||
break;
|
||
|
||
default:
|
||
throw new Error(`Unknown auth method: ${authData.method}`);
|
||
}
|
||
|
||
this.storage.setItem(this.storageKey, JSON.stringify(authState));
|
||
this.currentAuthState = authState;
|
||
console.log('AuthManager: Auth state saved for method:', authData.method);
|
||
|
||
} catch (error) {
|
||
console.error('AuthManager: Failed to save auth state:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Restore authentication state on page load
|
||
async restoreAuthState() {
|
||
try {
|
||
console.log('🔍 AuthManager: === restoreAuthState START ===');
|
||
console.log('🔍 AuthManager: storageKey:', this.storageKey);
|
||
|
||
const stored = this.storage.getItem(this.storageKey);
|
||
console.log('🔍 AuthManager: localStorage raw value:', stored);
|
||
|
||
if (!stored) {
|
||
console.log('🔍 AuthManager: ❌ No stored auth state found');
|
||
return null;
|
||
}
|
||
|
||
const authState = JSON.parse(stored);
|
||
console.log('🔍 AuthManager: ✅ Parsed stored auth state:', authState);
|
||
console.log('🔍 AuthManager: Method:', authState.method);
|
||
console.log('🔍 AuthManager: Timestamp:', authState.timestamp);
|
||
console.log('🔍 AuthManager: Age (ms):', Date.now() - authState.timestamp);
|
||
|
||
// Check if stored state is too old (24 hours for most methods, 1 hour for extensions)
|
||
const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
|
||
console.log('🔍 AuthManager: Max age for method:', maxAge, 'ms');
|
||
|
||
if (Date.now() - authState.timestamp > maxAge) {
|
||
console.log('🔍 AuthManager: ❌ Stored auth state expired, clearing');
|
||
this.clearAuthState();
|
||
return null;
|
||
}
|
||
|
||
console.log('🔍 AuthManager: ✅ Auth state not expired, attempting restore for method:', authState.method);
|
||
|
||
let result;
|
||
switch (authState.method) {
|
||
case 'extension':
|
||
console.log('🔍 AuthManager: Calling _restoreExtensionAuth...');
|
||
result = await this._restoreExtensionAuth(authState);
|
||
break;
|
||
|
||
case 'local':
|
||
console.log('🔍 AuthManager: Calling _restoreLocalAuth...');
|
||
result = await this._restoreLocalAuth(authState);
|
||
break;
|
||
|
||
case 'nip46':
|
||
console.log('🔍 AuthManager: Calling _restoreNip46Auth...');
|
||
result = await this._restoreNip46Auth(authState);
|
||
break;
|
||
|
||
case 'readonly':
|
||
console.log('🔍 AuthManager: Calling _restoreReadonlyAuth...');
|
||
result = await this._restoreReadonlyAuth(authState);
|
||
break;
|
||
|
||
default:
|
||
console.warn('🔍 AuthManager: ❌ Unknown auth method in stored state:', authState.method);
|
||
return null;
|
||
}
|
||
|
||
console.log('🔍 AuthManager: Restore method result:', result);
|
||
console.log('🔍 AuthManager: === restoreAuthState END ===');
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('🔍 AuthManager: ❌ Failed to restore auth state:', error);
|
||
console.error('🔍 AuthManager: Error stack:', error.stack);
|
||
this.clearAuthState(); // Clear corrupted state
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async _restoreExtensionAuth(authState) {
|
||
console.log('🔍 AuthManager: === _restoreExtensionAuth START ===');
|
||
console.log('🔍 AuthManager: authState:', authState);
|
||
console.log('🔍 AuthManager: window.nostr available:', !!window.nostr);
|
||
console.log('🔍 AuthManager: window.nostr constructor:', window.nostr?.constructor?.name);
|
||
|
||
// SMART EXTENSION WAITING SYSTEM
|
||
// Extensions often load after our library, so we need to wait for them
|
||
const extension = await this._waitForExtension(authState, 3000); // Wait up to 3 seconds
|
||
|
||
if (!extension) {
|
||
console.log('🔍 AuthManager: ❌ No extension found after waiting');
|
||
return null;
|
||
}
|
||
|
||
console.log('🔍 AuthManager: ✅ Extension found:', extension.constructor?.name);
|
||
|
||
try {
|
||
// Verify extension still works and has same pubkey
|
||
const currentPubkey = await extension.getPublicKey();
|
||
if (currentPubkey !== authState.pubkey) {
|
||
console.log('🔍 AuthManager: ❌ Extension pubkey changed, not restoring');
|
||
console.log('🔍 AuthManager: Expected:', authState.pubkey);
|
||
console.log('🔍 AuthManager: Got:', currentPubkey);
|
||
return null;
|
||
}
|
||
|
||
console.log('🔍 AuthManager: ✅ Extension auth restored successfully');
|
||
return {
|
||
method: 'extension',
|
||
pubkey: authState.pubkey,
|
||
extension: extension
|
||
};
|
||
|
||
} catch (error) {
|
||
console.log('🔍 AuthManager: ❌ Extension verification failed:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Smart extension waiting system - polls multiple locations for extensions
|
||
async _waitForExtension(authState, maxWaitMs = 3000) {
|
||
console.log('🔍 AuthManager: === _waitForExtension START ===');
|
||
console.log('🔍 AuthManager: maxWaitMs:', maxWaitMs);
|
||
console.log('🔍 AuthManager: Looking for extension with constructor:', authState.extensionVerification?.constructor);
|
||
|
||
const startTime = Date.now();
|
||
const pollInterval = 100; // Check every 100ms
|
||
|
||
// Extension locations to check (in priority order)
|
||
const extensionLocations = [
|
||
{ path: 'window.nostr', getter: () => window.nostr },
|
||
{ path: 'navigator.nostr', getter: () => navigator?.nostr },
|
||
{ path: 'window.navigator?.nostr', getter: () => window.navigator?.nostr },
|
||
{ path: 'window.alby?.nostr', getter: () => window.alby?.nostr },
|
||
{ path: 'window.webln?.nostr', getter: () => window.webln?.nostr },
|
||
{ path: 'window.nos2x', getter: () => window.nos2x },
|
||
{ path: 'window.flamingo?.nostr', getter: () => window.flamingo?.nostr },
|
||
{ path: 'window.mutiny?.nostr', getter: () => window.mutiny?.nostr }
|
||
];
|
||
|
||
while (Date.now() - startTime < maxWaitMs) {
|
||
console.log('🔍 AuthManager: Polling for extensions... (elapsed:', Date.now() - startTime, 'ms)');
|
||
|
||
// If our facade is currently installed and blocking, temporarily remove it
|
||
let facadeRemoved = false;
|
||
let originalNostr = null;
|
||
if (window.nostr?.constructor?.name === 'WindowNostr') {
|
||
console.log('🔍 AuthManager: Temporarily removing our facade to check for real extensions');
|
||
originalNostr = window.nostr;
|
||
window.nostr = window.nostr.existingNostr || undefined;
|
||
facadeRemoved = true;
|
||
}
|
||
|
||
try {
|
||
// Check all extension locations
|
||
for (const location of extensionLocations) {
|
||
try {
|
||
const extension = location.getter();
|
||
console.log('🔍 AuthManager: Checking', location.path, ':', !!extension, extension?.constructor?.name);
|
||
|
||
if (this._isValidExtensionForRestore(extension, authState)) {
|
||
console.log('🔍 AuthManager: ✅ Found matching extension at', location.path);
|
||
|
||
// Restore facade if we removed it
|
||
if (facadeRemoved && originalNostr) {
|
||
console.log('🔍 AuthManager: Restoring facade after finding extension');
|
||
window.nostr = originalNostr;
|
||
}
|
||
|
||
return extension;
|
||
}
|
||
} catch (error) {
|
||
console.log('🔍 AuthManager: Error checking', location.path, ':', error.message);
|
||
}
|
||
}
|
||
|
||
// Restore facade if we removed it and haven't found an extension yet
|
||
if (facadeRemoved && originalNostr) {
|
||
window.nostr = originalNostr;
|
||
facadeRemoved = false;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('🔍 AuthManager: Error during extension polling:', error);
|
||
|
||
// Restore facade if we removed it
|
||
if (facadeRemoved && originalNostr) {
|
||
window.nostr = originalNostr;
|
||
}
|
||
}
|
||
|
||
// Wait before next poll
|
||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||
}
|
||
|
||
console.log('🔍 AuthManager: ❌ Extension waiting timeout after', maxWaitMs, 'ms');
|
||
return null;
|
||
}
|
||
|
||
// Check if an extension is valid for restoration
|
||
_isValidExtensionForRestore(extension, authState) {
|
||
if (!extension || typeof extension !== 'object') {
|
||
return false;
|
||
}
|
||
|
||
// Must have required Nostr methods
|
||
if (typeof extension.getPublicKey !== 'function' ||
|
||
typeof extension.signEvent !== 'function') {
|
||
return false;
|
||
}
|
||
|
||
// Must not be our own classes
|
||
const constructorName = extension.constructor?.name;
|
||
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') {
|
||
return false;
|
||
}
|
||
|
||
// Must not be NostrTools
|
||
if (extension === window.NostrTools) {
|
||
return false;
|
||
}
|
||
|
||
// If we have stored verification data, check constructor match
|
||
const verification = authState.extensionVerification;
|
||
if (verification && verification.constructor) {
|
||
if (constructorName !== verification.constructor) {
|
||
console.log('🔍 AuthManager: Constructor mismatch -',
|
||
'expected:', verification.constructor,
|
||
'got:', constructorName);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
console.log('🔍 AuthManager: ✅ Extension validation passed for:', constructorName);
|
||
return true;
|
||
}
|
||
|
||
async _restoreLocalAuth(authState) {
|
||
if (!authState.encrypted) {
|
||
console.log('AuthManager: No encrypted data found for local auth');
|
||
return null;
|
||
}
|
||
|
||
// Get session password
|
||
const sessionPassword = sessionStorage.getItem('nostr_session_key');
|
||
if (!sessionPassword) {
|
||
console.log('AuthManager: Session password not found, cannot decrypt');
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
// Decrypt the secret key
|
||
const salt = CryptoUtils.base64ToArrayBuffer(authState.encrypted.salt);
|
||
const key = await CryptoUtils.deriveKey(sessionPassword, new Uint8Array(salt));
|
||
|
||
const encryptedData = CryptoUtils.base64ToArrayBuffer(authState.encrypted.data);
|
||
const iv = CryptoUtils.base64ToArrayBuffer(authState.encrypted.iv);
|
||
|
||
const secret = await CryptoUtils.decrypt(encryptedData, key, new Uint8Array(iv));
|
||
|
||
console.log('AuthManager: Local auth restored successfully');
|
||
return {
|
||
method: 'local',
|
||
pubkey: authState.pubkey,
|
||
secret: secret
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('AuthManager: Failed to decrypt local key:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async _restoreNip46Auth(authState) {
|
||
if (!authState.nip46) {
|
||
console.log('AuthManager: No NIP-46 data found');
|
||
return null;
|
||
}
|
||
|
||
// For NIP-46, we can't automatically restore the connection
|
||
// because it requires the user to re-authenticate with the remote signer
|
||
// Instead, we return the connection parameters so the UI can prompt for reconnection
|
||
console.log('AuthManager: NIP-46 connection data found, requires user reconnection');
|
||
return {
|
||
method: 'nip46',
|
||
pubkey: authState.pubkey,
|
||
requiresReconnection: true,
|
||
connectionData: authState.nip46
|
||
};
|
||
}
|
||
|
||
async _restoreReadonlyAuth(authState) {
|
||
console.log('AuthManager: Read-only auth restored successfully');
|
||
return {
|
||
method: 'readonly',
|
||
pubkey: authState.pubkey
|
||
};
|
||
}
|
||
|
||
// Clear stored authentication state
|
||
clearAuthState() {
|
||
this.storage.removeItem(this.storageKey);
|
||
sessionStorage.removeItem('nostr_session_key');
|
||
this.currentAuthState = null;
|
||
console.log('AuthManager: Auth state cleared');
|
||
}
|
||
|
||
// Generate a session-specific password for local key encryption
|
||
_generateSessionPassword() {
|
||
const array = new Uint8Array(32);
|
||
window.crypto.getRandomValues(array);
|
||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
// Check if we have valid stored auth
|
||
hasStoredAuth() {
|
||
const stored = this.storage.getItem(this.storageKey);
|
||
return !!stored;
|
||
}
|
||
|
||
// Get current auth method without full restoration
|
||
getStoredAuthMethod() {
|
||
try {
|
||
const stored = this.storage.getItem(this.storageKey);
|
||
if (!stored) return null;
|
||
|
||
const authState = JSON.parse(stored);
|
||
return authState.method;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// NIP-07 compliant window.nostr provider
|
||
class WindowNostr {
|
||
constructor(nostrLite, existingNostr = null) {
|
||
this.nostrLite = nostrLite;
|
||
this.authState = null;
|
||
this.existingNostr = existingNostr;
|
||
this.authenticatedExtension = null;
|
||
this.authManager = new AuthManager({ isolateSession: nostrLite.options?.isolateSession });
|
||
this._setupEventListeners();
|
||
}
|
||
|
||
_setupEventListeners() {
|
||
// Listen for authentication events to store auth state
|
||
if (typeof window !== 'undefined') {
|
||
window.addEventListener('nlMethodSelected', async (event) => {
|
||
this.authState = event.detail;
|
||
|
||
// If extension method, capture the specific extension the user chose
|
||
if (event.detail.method === 'extension') {
|
||
this.authenticatedExtension = event.detail.extension;
|
||
console.log('WindowNostr: Captured authenticated extension:', this.authenticatedExtension?.constructor?.name);
|
||
}
|
||
|
||
// Use global setAuthState function for unified persistence
|
||
try {
|
||
setAuthState(event.detail, { isolateSession: this.nostrLite.options?.isolateSession });
|
||
console.log('WindowNostr: Auth state saved via global setAuthState');
|
||
} catch (error) {
|
||
console.error('WindowNostr: Failed to save auth state via global setAuthState:', error);
|
||
}
|
||
|
||
// EXTENSION-FIRST: Only reinstall facade for non-extension methods
|
||
// Extensions handle their own window.nostr - don't interfere!
|
||
if (event.detail.method !== 'extension' && typeof window !== 'undefined') {
|
||
console.log('WindowNostr: Re-installing facade after', this.authState?.method, 'authentication');
|
||
window.nostr = this;
|
||
} else if (event.detail.method === 'extension') {
|
||
console.log('WindowNostr: Extension authentication - NOT reinstalling facade');
|
||
}
|
||
|
||
console.log('WindowNostr: Auth state updated:', this.authState?.method);
|
||
});
|
||
|
||
window.addEventListener('nlLogout', () => {
|
||
this.authState = null;
|
||
this.authenticatedExtension = null;
|
||
|
||
// Clear persistent auth state
|
||
this.authManager.clearAuthState();
|
||
console.log('WindowNostr: Auth state cleared and persistence removed');
|
||
|
||
// EXTENSION-FIRST: Only reinstall facade if we're not in extension mode
|
||
if (typeof window !== 'undefined' && !this.nostrLite?.hasExtension) {
|
||
console.log('WindowNostr: Re-installing facade after logout (non-extension mode)');
|
||
window.nostr = this;
|
||
} else {
|
||
console.log('WindowNostr: Logout in extension mode - NOT reinstalling facade');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Restore authentication state on page load
|
||
async restoreAuthState() {
|
||
try {
|
||
console.log('🔍 WindowNostr: === restoreAuthState START ===');
|
||
console.log('🔍 WindowNostr: authManager available:', !!this.authManager);
|
||
|
||
const restoredAuth = await this.authManager.restoreAuthState();
|
||
console.log('🔍 WindowNostr: authManager.restoreAuthState result:', restoredAuth);
|
||
|
||
if (restoredAuth) {
|
||
console.log('🔍 WindowNostr: ✅ Setting authState to restored auth');
|
||
this.authState = restoredAuth;
|
||
console.log('🔍 WindowNostr: this.authState now:', this.authState);
|
||
|
||
// Handle method-specific restoration
|
||
if (restoredAuth.method === 'extension') {
|
||
console.log('🔍 WindowNostr: Extension method - setting authenticatedExtension');
|
||
this.authenticatedExtension = restoredAuth.extension;
|
||
console.log('🔍 WindowNostr: authenticatedExtension set to:', this.authenticatedExtension);
|
||
}
|
||
|
||
console.log('🔍 WindowNostr: ✅ Authentication state restored successfully!');
|
||
console.log('🔍 WindowNostr: Method:', restoredAuth.method);
|
||
console.log('🔍 WindowNostr: Pubkey:', restoredAuth.pubkey);
|
||
|
||
// Dispatch restoration event so UI can update
|
||
if (typeof window !== 'undefined') {
|
||
console.log('🔍 WindowNostr: Dispatching nlAuthRestored event...');
|
||
const event = new CustomEvent('nlAuthRestored', {
|
||
detail: restoredAuth
|
||
});
|
||
console.log('🔍 WindowNostr: Event detail:', event.detail);
|
||
window.dispatchEvent(event);
|
||
console.log('🔍 WindowNostr: ✅ nlAuthRestored event dispatched');
|
||
}
|
||
|
||
console.log('🔍 WindowNostr: === restoreAuthState END (success) ===');
|
||
return restoredAuth;
|
||
} else {
|
||
console.log('🔍 WindowNostr: ❌ No authentication state to restore (null from authManager)');
|
||
console.log('🔍 WindowNostr: === restoreAuthState END (no restore) ===');
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error('🔍 WindowNostr: ❌ Failed to restore auth state:', error);
|
||
console.error('🔍 WindowNostr: Error stack:', error.stack);
|
||
console.log('🔍 WindowNostr: === restoreAuthState END (error) ===');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async getPublicKey() {
|
||
if (!this.authState) {
|
||
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
|
||
}
|
||
|
||
switch (this.authState.method) {
|
||
case 'extension':
|
||
// Use the captured authenticated extension, not current window.nostr
|
||
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
|
||
if (!ext) throw new Error('Extension not available');
|
||
return await ext.getPublicKey();
|
||
|
||
case 'local':
|
||
case 'nip46':
|
||
return this.authState.pubkey;
|
||
|
||
case 'readonly':
|
||
throw new Error('Read-only mode - cannot get public key');
|
||
|
||
default:
|
||
throw new Error(`Unsupported auth method: ${this.authState.method}`);
|
||
}
|
||
}
|
||
|
||
async signEvent(event) {
|
||
if (!this.authState) {
|
||
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
|
||
}
|
||
|
||
if (this.authState.method === 'readonly') {
|
||
throw new Error('Read-only mode - cannot sign events');
|
||
}
|
||
|
||
switch (this.authState.method) {
|
||
case 'extension':
|
||
// Use the captured authenticated extension, not current window.nostr
|
||
console.log('WindowNostr: signEvent - authenticatedExtension:', this.authenticatedExtension);
|
||
console.log('WindowNostr: signEvent - authState.extension:', this.authState.extension);
|
||
console.log('WindowNostr: signEvent - existingNostr:', this.existingNostr);
|
||
|
||
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
|
||
console.log('WindowNostr: signEvent - using extension:', ext);
|
||
console.log('WindowNostr: signEvent - extension constructor:', ext?.constructor?.name);
|
||
|
||
if (!ext) throw new Error('Extension not available');
|
||
return await ext.signEvent(event);
|
||
|
||
case 'local': {
|
||
// Use nostr-tools to sign with local secret key
|
||
const { nip19, finalizeEvent } = window.NostrTools;
|
||
let secretKey;
|
||
|
||
if (this.authState.secret.startsWith('nsec')) {
|
||
const decoded = nip19.decode(this.authState.secret);
|
||
secretKey = decoded.data;
|
||
} else {
|
||
// Convert hex to Uint8Array
|
||
secretKey = this._hexToUint8Array(this.authState.secret);
|
||
}
|
||
|
||
return finalizeEvent(event, secretKey);
|
||
}
|
||
|
||
case 'nip46': {
|
||
// Use BunkerSigner for NIP-46
|
||
if (!this.authState.signer?.bunkerSigner) {
|
||
throw new Error('NIP-46 signer not available');
|
||
}
|
||
return await this.authState.signer.bunkerSigner.signEvent(event);
|
||
}
|
||
|
||
default:
|
||
throw new Error(`Unsupported auth method: ${this.authState.method}`);
|
||
}
|
||
}
|
||
|
||
async getRelays() {
|
||
// Return default relays since we removed the relays configuration
|
||
return ['wss://relay.damus.io', 'wss://nos.lol'];
|
||
}
|
||
|
||
get nip04() {
|
||
return {
|
||
encrypt: async (pubkey, plaintext) => {
|
||
if (!this.authState) {
|
||
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
|
||
}
|
||
|
||
if (this.authState.method === 'readonly') {
|
||
throw new Error('Read-only mode - cannot encrypt');
|
||
}
|
||
|
||
switch (this.authState.method) {
|
||
case 'extension': {
|
||
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
|
||
if (!ext) throw new Error('Extension not available');
|
||
return await ext.nip04.encrypt(pubkey, plaintext);
|
||
}
|
||
|
||
case 'local': {
|
||
const { nip04, nip19 } = window.NostrTools;
|
||
let secretKey;
|
||
|
||
if (this.authState.secret.startsWith('nsec')) {
|
||
const decoded = nip19.decode(this.authState.secret);
|
||
secretKey = decoded.data;
|
||
} else {
|
||
secretKey = this._hexToUint8Array(this.authState.secret);
|
||
}
|
||
|
||
return await nip04.encrypt(secretKey, pubkey, plaintext);
|
||
}
|
||
|
||
case 'nip46': {
|
||
if (!this.authState.signer?.bunkerSigner) {
|
||
throw new Error('NIP-46 signer not available');
|
||
}
|
||
return await this.authState.signer.bunkerSigner.nip04Encrypt(pubkey, plaintext);
|
||
}
|
||
|
||
default:
|
||
throw new Error(`Unsupported auth method: ${this.authState.method}`);
|
||
}
|
||
},
|
||
|
||
decrypt: async (pubkey, ciphertext) => {
|
||
if (!this.authState) {
|
||
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
|
||
}
|
||
|
||
if (this.authState.method === 'readonly') {
|
||
throw new Error('Read-only mode - cannot decrypt');
|
||
}
|
||
|
||
switch (this.authState.method) {
|
||
case 'extension': {
|
||
const ext = this.authenticatedExtension || this.authState.extension || this.existingNostr;
|
||
if (!ext) throw new Error('Extension not available');
|
||
return await ext.nip04.decrypt(pubkey, ciphertext);
|
||
}
|
||
|
||
case 'local': {
|
||
const { nip04, nip19 } = window.NostrTools;
|
||
let secretKey;
|
||
|
||
if (this.authState.secret.startsWith('nsec')) {
|
||
const decoded = nip19.decode(this.authState.secret);
|
||
secretKey = decoded.data;
|
||
} else {
|
||
secretKey = this._hexToUint8Array(this.authState.secret);
|
||
}
|
||
|
||
return await nip04.decrypt(secretKey, pubkey, ciphertext);
|
||
}
|
||
|
||
case 'nip46': {
|
||
if (!this.authState.signer?.bunkerSigner) {
|
||
throw new Error('NIP-46 signer not available');
|
||
}
|
||
return await this.authState.signer.bunkerSigner.nip04Decrypt(pubkey, ciphertext);
|
||
}
|
||
|
||
default:
|
||
throw new Error(`Unsupported auth method: ${this.authState.method}`);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
get nip44() {
|
||
return {
|
||
encrypt: async (pubkey, plaintext) => {
|
||
const authState = getAuthState();
|
||
if (!authState) {
|
||
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
|
||
}
|
||
|
||
if (authState.method === 'readonly') {
|
||
throw new Error('Read-only mode - cannot encrypt');
|
||
}
|
||
|
||
switch (authState.method) {
|
||
case 'extension': {
|
||
const ext = this.authenticatedExtension || authState.extension || this.existingNostr;
|
||
if (!ext) throw new Error('Extension not available');
|
||
return await ext.nip44.encrypt(pubkey, plaintext);
|
||
}
|
||
|
||
case 'local': {
|
||
const { nip44, nip19 } = window.NostrTools;
|
||
let secretKey;
|
||
|
||
if (authState.secret.startsWith('nsec')) {
|
||
const decoded = nip19.decode(authState.secret);
|
||
secretKey = decoded.data;
|
||
} else {
|
||
secretKey = this._hexToUint8Array(authState.secret);
|
||
}
|
||
|
||
return nip44.encrypt(plaintext, nip44.getConversationKey(secretKey, pubkey));
|
||
}
|
||
|
||
case 'nip46': {
|
||
if (!authState.signer?.bunkerSigner) {
|
||
throw new Error('NIP-46 signer not available');
|
||
}
|
||
return await authState.signer.bunkerSigner.nip44Encrypt(pubkey, plaintext);
|
||
}
|
||
|
||
default:
|
||
throw new Error('Unsupported auth method: ' + authState.method);
|
||
}
|
||
},
|
||
|
||
decrypt: async (pubkey, ciphertext) => {
|
||
const authState = getAuthState();
|
||
if (!authState) {
|
||
throw new Error('Not authenticated - use NOSTR_LOGIN_LITE.launch()');
|
||
}
|
||
|
||
if (authState.method === 'readonly') {
|
||
throw new Error('Read-only mode - cannot decrypt');
|
||
}
|
||
|
||
switch (authState.method) {
|
||
case 'extension': {
|
||
const ext = this.authenticatedExtension || authState.extension || this.existingNostr;
|
||
if (!ext) throw new Error('Extension not available');
|
||
return await ext.nip44.decrypt(pubkey, ciphertext);
|
||
}
|
||
|
||
case 'local': {
|
||
const { nip44, nip19 } = window.NostrTools;
|
||
let secretKey;
|
||
|
||
if (authState.secret.startsWith('nsec')) {
|
||
const decoded = nip19.decode(authState.secret);
|
||
secretKey = decoded.data;
|
||
} else {
|
||
secretKey = this._hexToUint8Array(authState.secret);
|
||
}
|
||
|
||
return nip44.decrypt(ciphertext, nip44.getConversationKey(secretKey, pubkey));
|
||
}
|
||
|
||
case 'nip46': {
|
||
if (!authState.signer?.bunkerSigner) {
|
||
throw new Error('NIP-46 signer not available');
|
||
}
|
||
return await authState.signer.bunkerSigner.nip44Decrypt(pubkey, ciphertext);
|
||
}
|
||
|
||
default:
|
||
throw new Error('Unsupported auth method: ' + authState.method);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
_hexToUint8Array(hex) {
|
||
if (hex.length % 2 !== 0) {
|
||
throw new Error('Invalid hex string length');
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// Global Authentication State Manager - Single Source of Truth
|
||
// ======================================
|
||
|
||
// Storage-based authentication state - works regardless of extension presence
|
||
function getAuthState() {
|
||
try {
|
||
console.log('🔍 getAuthState: === GLOBAL AUTH STATE CHECK ===');
|
||
|
||
const storageKey = 'nostr_login_lite_auth';
|
||
let stored = null;
|
||
let storageType = null;
|
||
|
||
// Check sessionStorage first (per-window isolation), then localStorage
|
||
if (sessionStorage.getItem(storageKey)) {
|
||
stored = sessionStorage.getItem(storageKey);
|
||
storageType = 'sessionStorage';
|
||
console.log('🔍 getAuthState: Found auth in sessionStorage');
|
||
} else if (localStorage.getItem(storageKey)) {
|
||
stored = localStorage.getItem(storageKey);
|
||
storageType = 'localStorage';
|
||
console.log('🔍 getAuthState: Found auth in localStorage');
|
||
}
|
||
|
||
if (!stored) {
|
||
console.log('🔍 getAuthState: ❌ No stored auth state found');
|
||
return null;
|
||
}
|
||
|
||
const authState = JSON.parse(stored);
|
||
console.log('🔍 getAuthState: ✅ Parsed stored auth state from', storageType);
|
||
console.log('🔍 getAuthState: Method:', authState.method);
|
||
console.log('🔍 getAuthState: Pubkey:', authState.pubkey);
|
||
console.log('🔍 getAuthState: Age (ms):', Date.now() - authState.timestamp);
|
||
|
||
// Check if auth state is expired
|
||
const maxAge = authState.method === 'extension' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
|
||
if (Date.now() - authState.timestamp > maxAge) {
|
||
console.log('🔍 getAuthState: ❌ Auth state expired, clearing');
|
||
sessionStorage.removeItem(storageKey);
|
||
localStorage.removeItem(storageKey);
|
||
return null;
|
||
}
|
||
|
||
console.log('🔍 getAuthState: ✅ Valid auth state found');
|
||
return authState;
|
||
|
||
} catch (error) {
|
||
console.error('🔍 getAuthState: ❌ Error reading auth state:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// Global Authentication State Management - Unified Persistence
|
||
// ======================================
|
||
|
||
// Global setAuthState function for unified persistence across all authentication methods
|
||
function setAuthState(authData, options = {}) {
|
||
try {
|
||
console.log('🔍 setAuthState: === GLOBAL AUTH STATE SAVE ===');
|
||
console.log('🔍 setAuthState: authData:', authData);
|
||
console.log('🔍 setAuthState: options:', options);
|
||
|
||
const storageKey = 'nostr_login_lite_auth';
|
||
|
||
// Determine which storage to use based on isolateSession option
|
||
const storage = options.isolateSession ? sessionStorage : localStorage;
|
||
const storageType = options.isolateSession ? 'sessionStorage' : 'localStorage';
|
||
|
||
console.log('🔍 setAuthState: Using', storageType, 'for persistence');
|
||
|
||
// Create auth state object
|
||
const authState = {
|
||
method: authData.method,
|
||
timestamp: Date.now(),
|
||
pubkey: authData.pubkey
|
||
};
|
||
|
||
// Add method-specific data (but no secrets for extension method)
|
||
switch (authData.method) {
|
||
case 'extension':
|
||
// For extensions, only store verification data - no secrets
|
||
authState.extensionVerification = {
|
||
constructor: authData.extension?.constructor?.name,
|
||
hasGetPublicKey: typeof authData.extension?.getPublicKey === 'function',
|
||
hasSignEvent: typeof authData.extension?.signEvent === 'function'
|
||
};
|
||
console.log('🔍 setAuthState: Extension method - storing verification data only');
|
||
break;
|
||
|
||
case 'local':
|
||
// For local keys, store the secret (will be encrypted by AuthManager if needed)
|
||
if (authData.secret) {
|
||
authState.secret = authData.secret;
|
||
console.log('🔍 setAuthState: Local method - storing secret key');
|
||
}
|
||
break;
|
||
|
||
case 'nip46':
|
||
// For NIP-46, store connection parameters
|
||
if (authData.signer) {
|
||
authState.nip46 = {
|
||
remotePubkey: authData.signer.remotePubkey,
|
||
relays: authData.signer.relays,
|
||
// Don't store secret - user will need to reconnect
|
||
};
|
||
console.log('🔍 setAuthState: NIP-46 method - storing connection parameters');
|
||
}
|
||
break;
|
||
|
||
case 'readonly':
|
||
// Read-only mode has no additional data to store
|
||
console.log('🔍 setAuthState: Read-only method - storing basic auth state');
|
||
break;
|
||
|
||
default:
|
||
console.warn('🔍 setAuthState: Unknown auth method:', authData.method);
|
||
break;
|
||
}
|
||
|
||
// Store the auth state
|
||
storage.setItem(storageKey, JSON.stringify(authState));
|
||
console.log('🔍 setAuthState: ✅ Auth state saved successfully');
|
||
console.log('🔍 setAuthState: Final auth state:', authState);
|
||
|
||
return authState;
|
||
|
||
} catch (error) {
|
||
console.error('🔍 setAuthState: ❌ Error saving auth state:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// ======================================
|
||
// Global Authentication State Clearing
|
||
// ======================================
|
||
|
||
// Global clearAuthState function for unified auth state clearing
|
||
function clearAuthState() {
|
||
try {
|
||
console.log('🔍 clearAuthState: === GLOBAL AUTH STATE CLEAR ===');
|
||
|
||
const storageKey = 'nostr_login_lite_auth';
|
||
|
||
// Clear from both storage types to ensure complete cleanup
|
||
if (typeof sessionStorage !== 'undefined') {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem('nostr_session_key');
|
||
console.log('🔍 clearAuthState: ✅ Cleared auth state from sessionStorage');
|
||
}
|
||
|
||
if (typeof localStorage !== 'undefined') {
|
||
localStorage.removeItem(storageKey);
|
||
console.log('🔍 clearAuthState: ✅ Cleared auth state from localStorage');
|
||
}
|
||
|
||
console.log('🔍 clearAuthState: ✅ All auth state cleared successfully');
|
||
|
||
} catch (error) {
|
||
console.error('🔍 clearAuthState: ❌ Error clearing auth state:', error);
|
||
}
|
||
}
|
||
|
||
|
||
// Initialize and export
|
||
if (typeof window !== 'undefined') {
|
||
const nostrLite = new NostrLite();
|
||
|
||
// Export main API
|
||
window.NOSTR_LOGIN_LITE = {
|
||
init: (options) => nostrLite.init(options),
|
||
launch: (startScreen) => nostrLite.launch(startScreen),
|
||
logout: () => nostrLite.logout(),
|
||
|
||
// Embedded modal method
|
||
embed: (container, options) => nostrLite.embed(container, options),
|
||
|
||
// CSS-only theme management API
|
||
switchTheme: (themeName) => nostrLite.switchTheme(themeName),
|
||
getCurrentTheme: () => nostrLite.getCurrentTheme(),
|
||
getAvailableThemes: () => nostrLite.getAvailableThemes(),
|
||
|
||
// Floating tab management API
|
||
showFloatingTab: () => nostrLite.showFloatingTab(),
|
||
hideFloatingTab: () => nostrLite.hideFloatingTab(),
|
||
toggleFloatingTab: () => nostrLite.toggleFloatingTab(),
|
||
updateFloatingTab: (options) => nostrLite.updateFloatingTab(options),
|
||
getFloatingTabState: () => nostrLite.getFloatingTabState(),
|
||
|
||
// GLOBAL AUTHENTICATION STATE API - Single Source of Truth
|
||
getAuthState: getAuthState,
|
||
setAuthState: setAuthState,
|
||
clearAuthState: clearAuthState,
|
||
|
||
// Expose for debugging
|
||
_extensionBridge: nostrLite.extensionBridge,
|
||
_instance: nostrLite
|
||
};
|
||
|
||
console.log('NOSTR_LOGIN_LITE: Library loaded and ready');
|
||
console.log('NOSTR_LOGIN_LITE: Use window.NOSTR_LOGIN_LITE.init(options) to initialize');
|
||
console.log('NOSTR_LOGIN_LITE: Detected', nostrLite.extensionBridge.getExtensionCount(), 'browser extensions');
|
||
} else {
|
||
// Node.js environment
|
||
module.exports = { NostrLite };
|
||
}
|