This commit is contained in:
n 2025-04-12 20:04:12 +01:00
parent 6da0b87fba
commit 79e65e3860
44 changed files with 1066 additions and 1387 deletions

@ -38,6 +38,11 @@
<div id="billboardRelayStatus" class="relay-status">Not connected</div>
</div>
<div class="billboard-actions">
<div class="login-container">
<div id="login-status">Not logged in.</div>
<button id="login-button" class="auth-button">Login with Nostr</button>
<button id="logout-button" class="auth-button" style="display: none;">Logout</button>
</div>
<button id="createBillboardBtn" class="primary-button">+ Add New Billboard</button>
</div>
</div>

@ -1,9 +1,8 @@
/**
* auth-manager.ts
* Centralized authentication manager for the entire application.
* Prevents network requests until the user is properly authenticated.
*
* Updated to use the new AuthenticationService for more robust authentication.
* Refactored to remove localStorage and session dependencies.
* Uses direct Nostr extension checks to determine auth state.
*/
import { AuthenticationService } from './services/AuthenticationService';
@ -11,12 +10,6 @@ import { AuthenticationService } from './services/AuthenticationService';
// Create singleton instance of AuthenticationService
const authService = new AuthenticationService();
// For backward compatibility
let userAuthenticated = false;
// Event name for authentication state changes
const AUTH_STATE_CHANGED_EVENT = 'auth-state-changed';
// Direct listeners registry for auth state changes
type AuthStateListener = (authenticated: boolean, pubkey?: string) => void;
const authStateListeners: AuthStateListener[] = [];
@ -37,52 +30,76 @@ function notifyAuthStateListeners(authenticated: boolean, pubkey?: string): void
}
}
/**
* Update the login status in UI elements
*/
function updateLoginStatus(pubkey: string | null): void {
const statusElement = document.getElementById('login-status');
const loginButton = document.getElementById('login-button');
const logoutButton = document.getElementById('logout-button');
if (statusElement && loginButton && logoutButton) {
if (pubkey) {
statusElement.textContent = `Logged in as: ${pubkey.substring(0, 8)}...`;
loginButton.style.display = 'none';
logoutButton.style.display = 'inline-block';
} else {
statusElement.textContent = 'Not logged in.';
loginButton.style.display = 'inline-block';
logoutButton.style.display = 'none';
}
}
}
// Queue for operations that should run after authentication
const postAuthQueue: Array<() => void> = [];
/**
* Check if the user is currently authenticated
* Makes a direct check to the extension without using localStorage
*
* @returns Promise<boolean> indicating if user is authenticated
*/
export function isAuthenticated(): boolean {
// Use the AuthenticationService
const result = authService.isAuthenticated();
// Update the backward-compatible flag
userAuthenticated = result;
return result;
export async function checkAuthentication(): Promise<boolean> {
return authService.checkAuthentication();
}
/**
* Set the authentication state
* Synchronous check if user is authenticated (uses cached state)
* This doesn't make a new request to the extension
*
* @returns boolean indicating if user appears to be authenticated
*/
export function setAuthenticated(authenticated: boolean, pubkey?: string): void {
const previousState = userAuthenticated;
userAuthenticated = authenticated;
if (authenticated && pubkey) {
// For now, just store the pubkey in the AuthenticationService without full authentication
// This maintains backward compatibility while allowing us to use the newer service
const currentSession = authService.getCurrentSession();
export function isAuthenticated(): boolean {
return authService.isAuthenticated();
}
/**
* Login with NostrLogin
* @returns Promise resolving to user's pubkey or null if login failed
*/
export async function loginWithNostrLogin(): Promise<string | null> {
try {
const result = await authService.authenticate();
// If there's no active session, we need to create one
if (!currentSession) {
// This will trigger a full authentication flow when the user next performs an action
// requiring signature verification
localStorage.setItem('userPublicKey', pubkey);
if (result.success && result.pubkey) {
// Update UI
updateLoginStatus(result.pubkey);
// Notify listeners
notifyAuthStateListeners(true, result.pubkey);
// Execute queued operations
executePostAuthQueue();
return result.pubkey;
}
} else if (!authenticated) {
// Log out through the AuthenticationService
authService.logout();
return null;
} catch (error) {
console.error("Error using Nostr login:", error);
return null;
}
// Execute queued operations if becoming authenticated
if (!previousState && authenticated) {
executePostAuthQueue();
}
// Notify listeners of state change
notifyAuthStateListeners(authenticated, pubkey);
}
/**
@ -90,7 +107,7 @@ export function setAuthenticated(authenticated: boolean, pubkey?: string): void
* This ensures the operation only runs after successful authentication
*/
export function addPostAuthOperation(operation: () => void): void {
if (isAuthenticated()) {
if (authService.isAuthenticated()) {
// If already authenticated, run immediately
operation();
} else {
@ -121,26 +138,15 @@ function executePostAuthQueue(): void {
/**
* Listen for authentication state changes
* Supports both direct callback registration and CustomEvent listeners
*/
export function onAuthStateChanged(callback: AuthStateListener): () => void {
// Add to direct listeners
authStateListeners.push(callback);
// Set up CustomEvent listener for backward compatibility
const handler = (event: Event) => {
const authEvent = event as CustomEvent;
callback(
authEvent.detail.authenticated,
authEvent.detail.pubkey
);
};
// Also register with the auth service
const unsubscribe = authService.onAuthStateChanged(callback);
if (typeof window !== 'undefined') {
window.addEventListener(AUTH_STATE_CHANGED_EVENT, handler);
}
// Return a function to remove both listeners
// Return a function to remove the listener
return () => {
// Remove from direct listeners
const index = authStateListeners.indexOf(callback);
@ -148,10 +154,8 @@ export function onAuthStateChanged(callback: AuthStateListener): () => void {
authStateListeners.splice(index, 1);
}
// Remove CustomEvent listener
if (typeof window !== 'undefined') {
window.removeEventListener(AUTH_STATE_CHANGED_EVENT, handler);
}
// Also unsubscribe from auth service
unsubscribe();
};
}
@ -162,126 +166,185 @@ export function clearAuthState(): void {
// Use the AuthenticationService to log out
authService.logout();
// Update the backward-compatible flag
userAuthenticated = false;
// Update UI
updateLoginStatus(null);
// Clear temporary auth state in sessionStorage
[
'nostrLoginInitialized',
'nostrAuthInProgress',
'nostrLoginState',
'nostrAuthPending',
'nostrLoginStarted'
].forEach(key => {
sessionStorage.removeItem(key);
});
// Notify listeners
notifyAuthStateListeners(false);
}
/**
* Update authentication state from localStorage
* This should be called on page load
*/
export function updateAuthStateFromStorage(): void {
// Let the AuthenticationService handle loading from storage
// It will check its own storage first, then fall back to the old localStorage key
// If we're not authenticated in the new service but have a key in the old format,
// use that to restore state
if (!authService.isAuthenticated()) {
const savedPubkey = localStorage.getItem('userPublicKey');
if (savedPubkey) {
userAuthenticated = true;
}
} else {
userAuthenticated = true;
}
}
// Initialize authentication state from localStorage
// This ensures the auth state is set correctly when the module is loaded
updateAuthStateFromStorage();
// Initialize authentication when the page loads to ensure consistency
if (typeof window !== 'undefined') {
window.addEventListener('DOMContentLoaded', () => {
updateAuthStateFromStorage();
});
}
// Listen for page unload to cleanup authentication state
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
// Don't clear authentication state, just session-specific flags
sessionStorage.removeItem('nostrLoginInitialized');
// Clear any temporary auth state in sessionStorage
[
'nostrAuthInProgress',
'nostrLoginState',
'nostrAuthPending',
'nostrLoginStarted'
].forEach(key => {
sessionStorage.removeItem(key);
});
});
}
// Listen for auth state changes from the AuthenticationService
authService.onAuthStateChanged((authenticated, pubkey) => {
userAuthenticated = authenticated;
// Dispatch the legacy event for backward compatibility
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent(AUTH_STATE_CHANGED_EVENT, {
detail: { authenticated, pubkey }
})
);
}
// If becoming authenticated, execute queued operations
if (authenticated) {
executePostAuthQueue();
}
});
/**
* Get the current user's public key
* This attempts to fetch it from the extension if not already cached
*
* @returns Promise resolving to the user's pubkey or null
*/
export async function getCurrentUserPubkeyAsync(): Promise<string | null> {
return authService.getPubkey();
}
/**
* Get the current user's public key (synchronous version)
* This only returns the cached pubkey and doesn't make a new request
*
* @returns The cached pubkey or null
*/
export function getCurrentUserPubkey(): string | null {
return authService.getCurrentUserPubkey();
}
/**
* Create an authenticated HTTP request using NIP-98
* Initialize login elements in the UI
*/
export function initializeLoginUI(): void {
if (typeof window === 'undefined' || !window.document) {return;}
const loginButton = document.getElementById('login-button');
const logoutButton = document.getElementById('logout-button');
if (loginButton) {
loginButton.addEventListener('click', async () => {
const pubkey = await loginWithNostrLogin();
if (pubkey) {
console.log(`Login successful: ${pubkey.substring(0, 8)}...`);
}
});
}
if (logoutButton) {
logoutButton.addEventListener('click', () => {
clearAuthState();
});
}
// Update UI based on current state
authService.getPubkey().then(pubkey => {
updateLoginStatus(pubkey);
});
}
// Initialize login UI when DOM is loaded
if (typeof window !== 'undefined' && window.document) {
window.addEventListener('DOMContentLoaded', () => {
initializeLoginUI();
// Check authentication status on page load
checkAuthentication().then(isAuth => {
if (isAuth) {
getCurrentUserPubkeyAsync().then(pubkey => {
if (pubkey) {
updateLoginStatus(pubkey);
}
});
} else {
updateLoginStatus(null);
}
});
});
// Check authentication when returning to the page
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
console.log('[AUTH-MANAGER] Page visibility changed to visible, checking auth state');
checkAuthentication().then(isAuth => {
if (isAuth) {
getCurrentUserPubkeyAsync().then(pubkey => {
if (pubkey) {
updateLoginStatus(pubkey);
}
});
} else {
updateLoginStatus(null);
}
});
}
});
}
/**
* Update authentication state
* This is for backwards compatibility with existing code
* In the new implementation, we check with the extension directly instead of relying on storage
*/
export function updateAuthStateFromStorage(): void {
console.log('[AUTH-MANAGER] Checking authentication with extension');
// Check with the extension directly
checkAuthentication().then(isAuth => {
if (isAuth) {
getCurrentUserPubkeyAsync().then(pubkey => {
if (pubkey) {
console.log(`[AUTH-MANAGER] Found active pubkey: ${pubkey.substring(0, 8)}...`);
updateLoginStatus(pubkey);
// Ensure the global window property is set
if (window) {
window.nostrPubkey = pubkey;
}
// Notify listeners
notifyAuthStateListeners(true, pubkey);
}
});
} else {
console.log('[AUTH-MANAGER] No active pubkey found');
updateLoginStatus(null);
}
});
}
/**
* Set the authentication state
* This is for backwards compatibility with existing code
*
* @param authenticated Whether the user is authenticated
* @param pubkey The user's public key (if authenticated)
*/
export function setAuthenticated(authenticated: boolean, pubkey?: string): void {
if (authenticated && pubkey) {
// Update in-memory state in AuthenticationService
authService['cachedPubkey'] = pubkey;
// Update UI
updateLoginStatus(pubkey);
// Ensure the global window property is set
if (window) {
window.nostrPubkey = pubkey;
}
// Execute queued operations
executePostAuthQueue();
// Notify listeners
notifyAuthStateListeners(true, pubkey);
} else if (!authenticated) {
// Log out through the AuthenticationService
authService.logout();
// Update UI
updateLoginStatus(null);
// Notify listeners
notifyAuthStateListeners(false);
}
}
/**
* Create an authenticated HTTP request using NIP-98 (stub)
* @deprecated This method is maintained only for backwards compatibility
* @param method HTTP method
* @param url Request URL
* @param payload Optional request payload
* @returns Headers object with Authorization header, or null if not authenticated
* @returns Always returns null
*/
export async function createAuthHeaders(
method: string,
url: string,
payload?: string
): Promise<Headers | null> {
if (!isAuthenticated()) {
return null;
}
// Use the AuthenticationService to create a NIP-98 auth event
const authEvent = await authService.createAuthEvent(
method as any,
url,
payload
);
if (!authEvent) {
return null;
}
// Create headers from the auth event
return authService.createAuthHeaders(authEvent);
console.warn('auth-manager.createAuthHeaders is deprecated and will always return null');
return null;
}
/**

@ -3,10 +3,14 @@
import * as nostrTools from 'nostr-tools';
import * as authManager from './auth-manager';
import { defaultServerConfig } from './config';
import { NostrService } from './services/NostrService';
import { toggleTheme } from './theme-utils';
import * as authManager from './auth-manager';
// Import nostr-login using CommonJS pattern to ensure it's available
const NostrLogin = typeof require !== 'undefined' ? require('nostr-login') : null;
// Module-level variables
let nostrService: NostrService;
@ -23,6 +27,40 @@ document.addEventListener('DOMContentLoaded', () => {
// Make sure authentication state is fresh from localStorage
authManager.updateAuthStateFromStorage();
// Initialize the login UI elements
authManager.initializeLoginUI();
// Add event listener to login button
const loginButton = document.getElementById('login-button');
if (loginButton) {
loginButton.addEventListener('click', async () => {
console.log('Login button clicked');
try {
// Show login in progress
const statusElement = document.getElementById('login-status');
if (statusElement) {
statusElement.textContent = 'Logging in...';
}
// Try to authenticate with the improved auth-manager
console.log('Attempting to login with AuthManager...');
const pubkey = await authManager.loginWithNostrLogin();
if (pubkey) {
console.log(`Login successful with pubkey: ${pubkey}`);
updateUIBasedOnAuth();
} else {
console.log('Login attempt failed, showing error');
alert('Login failed. Please make sure you have a Nostr extension installed (like Alby or nos2x).');
}
} catch (error) {
console.error('Authentication error:', error);
alert(`Authentication error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
});
}
// Initialize services
// Create NostrService with a status update callback
nostrService = new NostrService((message: string, className: string) => {
@ -36,7 +74,6 @@ document.addEventListener('DOMContentLoaded', () => {
// Auto-connect to the default relay after a brief delay
setTimeout(autoConnectToDefaultRelay, 500);
setTimeout(autoConnectToDefaultRelay, 500);
});
/**
@ -49,6 +86,13 @@ function updateUIBasedOnAuth(): void {
// First refresh auth state from storage to ensure it's current
authManager.updateAuthStateFromStorage();
// Try to restore authentication state from localStorage if available
const savedPubkey = localStorage.getItem('userPublicKey');
if (savedPubkey && !authManager.isAuthenticated()) {
console.log("UpdateUI: Found pubkey in localStorage but not authenticated, attempting to restore auth state");
authManager.setAuthenticated(true, savedPubkey);
}
// Enable or disable the create button based on auth state
if (authManager.isAuthenticated()) {
createBillboardBtn.removeAttribute('disabled');
@ -94,45 +138,53 @@ function setupUIElements(): void {
authManager.updateAuthStateFromStorage();
// Check if user is authenticated
console.log("Billboard: Checking auth state...");
// Try to restore authentication state from localStorage if available
const savedPubkey = localStorage.getItem('userPublicKey');
if (savedPubkey && !authManager.isAuthenticated()) {
console.log("Billboard: Found pubkey in localStorage but not authenticated, attempting to restore auth state");
authManager.setAuthenticated(true, savedPubkey);
}
// Check authentication again after potential restoration
if (!authManager.isAuthenticated()) {
if (window.nostr) {
// If nostr extension is available, try to authenticate using our improved service
try {
const result = await authManager.authService.authenticate();
if (result.success && result.session) {
console.log(`Successfully authenticated with pubkey: ${result.session.pubkey.substring(0, 8)}...`);
handleCreateBillboard();
return;
}
} catch (error) {
console.error("Error during authentication:", error);
}
try {
console.log("Attempting authentication with NostrLogin...");
// Fall back to basic authentication if needed
try {
const pubkey = await window.nostr.getPublicKey();
if (pubkey) {
// Set authentication and proceed
authManager.setAuthenticated(true, pubkey);
console.log(`Direct login successful, pubkey: ${pubkey.substring(0, 8)}...`);
handleCreateBillboard();
return;
// Use the NostrLogin integration
const pubkey = await authManager.loginWithNostrLogin();
if (pubkey) {
console.log(`Successfully authenticated with pubkey: ${pubkey.substring(0, 8)}...`);
updateUIBasedOnAuth();
handleCreateBillboard();
return;
} else {
// If NostrLogin failed, try to authenticate using extensions directly
if (window.nostr) {
try {
const pubkey = await window.nostr.getPublicKey();
if (pubkey) {
// Set authentication and proceed
authManager.setAuthenticated(true, pubkey);
console.log(`Direct login successful, pubkey: ${pubkey.substring(0, 8)}...`);
updateUIBasedOnAuth();
handleCreateBillboard();
return;
}
} catch (error) {
console.error("Error getting pubkey from extension:", error);
}
}
} catch (error) {
console.error("Error getting pubkey from extension:", error);
console.warn('Authentication failed when trying to create billboard');
return;
}
} catch (error) {
console.error("Error during authentication:", error);
return;
}
// If we get here, authentication failed - show a more helpful message
const promptResult = confirm(
'You need to be logged in to create a billboard.\nWould you like to go to the Profile page to log in now?'
);
if (promptResult) {
// Redirect to profile page
window.location.href = 'profile.html';
}
return;
}
handleCreateBillboard();
@ -207,20 +259,26 @@ function updateRelayStatus(message: string, className: string): void {
async function handleConnectRelay(): Promise<void> {
// Check if user is authenticated
if (!authManager.isAuthenticated()) {
updateRelayStatus('Authentication required to connect', 'error');
// Display an authentication prompt
const billboardContent = document.getElementById('billboardContent');
if (billboardContent) {
billboardContent.innerHTML = `
<div class="auth-required-message">
<h3>Authentication Required</h3>
<p>You need to be logged in to connect to relays.</p>
<p>Please visit the <a href="profile.html">Profile page</a> to log in with your Nostr extension.</p>
</div>
`;
try {
// Use AuthManager to authenticate
updateRelayStatus('Connecting to Nostr extension...', 'connecting');
// Try to authenticate using our improved auth-manager
const pubkey = await authManager.loginWithNostrLogin();
if (pubkey) {
// Authentication successful
updateUIBasedOnAuth();
updateRelayStatus('Authentication successful. Connecting...', 'connecting');
} else {
updateRelayStatus('Authentication failed. Please try again or install a Nostr extension like Alby or nos2x.', 'error');
return;
}
} catch (error) {
console.error("Error during authentication:", error);
updateRelayStatus('Authentication error. Please check your Nostr extension.', 'error');
return;
}
return;
}
const relayUrlInput = document.getElementById('billboardRelayUrl') as HTMLInputElement;
@ -431,11 +489,28 @@ async function loadEventsFromCache(userPubkeyHex: string | null, showAllEvents:
* Process a server advertisement event (kind 31120)
* Open the modal for creating a new billboard
*/
function handleCreateBillboard(): void {
// Check if user is logged in
async function handleCreateBillboard(): Promise<void> {
// First check if user is logged in
authManager.updateAuthStateFromStorage();
if (!authManager.isAuthenticated()) {
alert('You need to be logged in to create a billboard. Please visit the Profile page to log in.');
return;
// If user isn't logged in, try to authenticate with NostrLogin
try {
console.log("Authentication attempt from handleCreateBillboard");
const pubkey = await authManager.loginWithNostrLogin();
if (pubkey) {
// Update authentication state
updateUIBasedOnAuth();
} else {
console.warn('Authentication failed when trying to create billboard');
return;
}
} catch (error) {
console.error("Error during authentication:", error);
return;
}
}
// Reset form
@ -536,8 +611,23 @@ async function handleSaveBillboard(e: Event): Promise<void> {
try {
// Check if user is logged in using auth manager
if (!authManager.isAuthenticated()) {
alert('You need to be logged in to publish a billboard. Please visit the Profile page to log in.');
return;
// Try to authenticate with NostrLogin
try {
console.log("Authentication attempt from handleSaveBillboard");
const pubkey = await authManager.loginWithNostrLogin();
if (pubkey) {
// Update UI
updateUIBasedOnAuth();
} else {
console.warn('Authentication failed when trying to publish billboard');
return;
}
} catch (error) {
console.error("Error during authentication:", error);
return;
}
}
// Get user's pubkey
@ -813,14 +903,30 @@ function processServerEvent(event: nostrTools.Event): void {
// Add event listener for the "Edit" button
const editBtn = eventCard.querySelector('.billboard-edit-btn');
if (editBtn) {
editBtn.addEventListener('click', (e) => {
editBtn.addEventListener('click', async (e) => {
const target = e.target as HTMLElement;
const eventId = target.getAttribute('data-id');
if (eventId) {
// Check if user is logged in
const loggedInPubkey = nostrService.getLoggedInPubkey();
if (!loggedInPubkey) {
alert('You need to be logged in to edit a billboard. Please visit the Profile page to log in.');
// Try to authenticate with NostrLogin
try {
console.log("Authentication attempt from edit button");
const pubkey = await authManager.loginWithNostrLogin();
if (pubkey) {
console.log(`Successfully authenticated with pubkey: ${pubkey.substring(0, 8)}...`);
updateUIBasedOnAuth();
handleEditBillboard(event);
return;
} else {
console.warn('Authentication failed when trying to edit billboard');
}
} catch (error) {
console.error("Error during authentication:", error);
}
return;
}
@ -865,7 +971,16 @@ async function autoConnectToDefaultRelay(): Promise<void> {
// Update auth status from storage
authManager.updateAuthStateFromStorage();
// Try to authenticate directly with extension if possible
// Check if we have a saved pubkey in localStorage but aren't authenticated
const savedPubkey = localStorage.getItem('userPublicKey');
if (savedPubkey && !authManager.isAuthenticated()) {
console.log(`AutoConnect: Found saved pubkey in localStorage: ${savedPubkey.substring(0, 8)}...`);
console.log("AutoConnect: Setting authenticated state based on localStorage");
authManager.setAuthenticated(true, savedPubkey);
updateUIBasedOnAuth();
}
// If still not authenticated and nostr is available, try direct authentication
if (!authManager.isAuthenticated() && window.nostr) {
try {
console.log('Attempting direct authentication with Nostr extension...');
@ -938,70 +1053,10 @@ async function autoConnectToDefaultRelay(): Promise<void> {
connectButton.click();
}
} else {
console.log('User is not authenticated, showing login prompt...');
// Show a login prompt with direct login option
const billboardContent = document.getElementById('billboardContent');
console.log('User is not authenticated, skipping auto-connect...');
// Only show the login prompt if no events were loaded from cache
if (billboardContent && billboardContent.querySelector('.empty-state')) {
billboardContent.innerHTML = `
<div class="auth-required-message">
<h3>Authentication Required</h3>
<p>You need to be logged in to view and manage billboards.</p>
<p>
<button id="directLoginBtn" class="primary-button">Login with Nostr Extension</button>
or visit the <a href="profile.html">Profile page</a> to login.
</p>
</div>
`;
// Add event listener to the direct login button
const directLoginBtn = document.getElementById('directLoginBtn');
if (directLoginBtn) {
directLoginBtn.addEventListener('click', async () => {
if (window.nostr) {
try {
directLoginBtn.textContent = 'Authenticating...';
directLoginBtn.setAttribute('disabled', 'true');
// Try to authenticate using our improved service
const result = await authManager.authService.authenticate();
if (result.success && result.session) {
console.log(`Direct login successful using authService, pubkey: ${result.session.pubkey.substring(0, 8)}...`);
// Update UI and reload
updateUIBasedOnAuth();
window.location.reload();
return;
}
// Fall back to basic pubkey retrieval
const pubkey = await window.nostr.getPublicKey();
if (pubkey) {
authManager.setAuthenticated(true, pubkey);
localStorage.setItem('userPublicKey', pubkey);
console.log(`Direct login successful, pubkey: ${pubkey.substring(0, 8)}...`);
// Update UI and connect
updateUIBasedOnAuth();
// Reload the page to properly initialize
window.location.reload();
}
} catch (error) {
console.error('Error during direct login:', error);
directLoginBtn.textContent = 'Login Failed - Try Again';
directLoginBtn.removeAttribute('disabled');
}
} else {
alert('No Nostr extension detected. Please install NIP-07 compatible extension like nos2x or Alby.');
}
});
}
}
// Update the relay status
updateRelayStatus('Authentication required', 'warning');
// Just update the relay status without showing the auth required message
updateRelayStatus('Authentication required to connect', 'warning');
}
}
}
@ -1012,7 +1067,7 @@ async function autoConnectToDefaultRelay(): Promise<void> {
* @returns Server pubkey or null if not found
*/
function getServerPubkeyFromEvent(event: nostrTools.Event): string | null {
if (event.kind !== 31120) return null;
if (event.kind !== 31120) {return null;}
// Find the d tag which contains the server pubkey
const dTag = event.tags.find(tag => tag[0] === 'd');

@ -9,10 +9,10 @@
* 4. Listening for incoming 21121 responses and associating them with requests
*/
import { ClientEventStore, ClientEventStatus } from './services/ClientEventStore';
import { ClientEventsTable } from './components/ClientEventsTable';
import type { NostrEvent } from './relay';
import { NostrRelayService } from './services/NostrRelayService';
import { ClientEventStore, ClientEventStatus } from './services/ClientEventStore';
import type { NostrRelayService } from './services/NostrRelayService';
// Create a singleton instance of the client event store
const clientEventStore = new ClientEventStore();

@ -3,7 +3,6 @@
// IMPORTANT: Immediately import all critical modules and execute them
// This ensures they are included in the bundle and executed immediately
import './navbar-diagnostics'; // Import diagnostics first
import './navbar';
import './navbar-init';
@ -37,16 +36,18 @@ import './navbar-init';
import * as nostrTools from 'nostr-tools';
// Import type definitions
import type { NostrEvent } from './converter';
// Import functions from internal modules
import { displayConvertedEvent } from './converter';
// Import client events tracking system
import * as authManager from './auth-manager';
import {
initClientEventHandler,
trackOutgoingEvent,
handleIncomingResponse,
reconnectRelayService
} from './client-event-handler';
import type { NostrEvent } from './converter';
// Import functions from internal modules
import { displayConvertedEvent } from './converter';
// Import client events tracking system
import { initializeNavbar } from './navbar';
import { publishToRelay, convertNpubToHex, verifyEvent } from './relay';
// Import profile functions (not using direct imports since we'll load modules based on page)
// This ensures all page modules are included in the bundle
@ -54,11 +55,9 @@ import './profile';
import './receiver'; // Import receiver module for relay connections and subscriptions
import './billboard'; // Import billboard module for server registration display
import { HttpFormatter } from './services/HttpFormatter'; // Import formatter for HTTP content
import { NostrService } from './services/NostrService';
import { Nostr31120Service } from './services/Nostr31120Service'; // Import our new dedicated service
import { NostrService } from './services/NostrService';
import { getUserPubkey } from './services/NostrUtils';
import { initializeNavbar } from './navbar';
import * as authManager from './auth-manager';
// Immediately initialize the navbar to ensure it's visible on page load
try {
@ -68,6 +67,7 @@ try {
console.error('[CLIENT] Error initializing navbar:', e);
}
import { toggleTheme } from './theme-utils';
import {
sanitizeText,
setDefaultHttpRequest,
@ -78,7 +78,6 @@ import {
} from './utils';
// Import toggleTheme from theme-utils instead of utils
import { toggleTheme } from './theme-utils';
// On page load, always fetch the latest pubkey from window.nostr
getUserPubkey().then(pubkey => {
@ -157,7 +156,7 @@ async function handleRelaySearch(): Promise<void> {
if (!authManager.isAuthenticated()) {
console.log('Cannot search relay: User not authenticated');
// Display authentication prompt instead of performing search
await retryAuthentication();
await loginWithNostr();
// Check if authentication was successful
if (!authManager.isAuthenticated()) {
@ -591,94 +590,45 @@ function loadSavedServer(): void {
}
/**
* Initialize nostr-login
* Prevents multiple initializations by checking if it's already been initialized
* Initialize direct interaction with Nostr extension
* Simplified to only check extension availability and current auth state
*/
function initNostrLogin(): void {
// Check if we've already initialized NostrLogin in this session
const nostrLoginInitialized = window.sessionStorage.getItem('nostrLoginInitialized');
if (nostrLoginInitialized === 'true') {
console.log("NostrLogin already initialized in this session, skipping");
return;
try {
// Simply check if we're already authenticated with the extension
if (window.nostr) {
console.log("Checking Nostr extension for authentication state...");
window.nostr.getPublicKey().then(pubkey => {
if (pubkey) {
console.log(`Found pubkey from extension: ${pubkey.slice(0, 8)}...`);
// Update UI to show pubkey
updateClientPubkeyDisplay(pubkey);
// Update auth state in the service
authManager.setAuthenticated(true, pubkey);
}
}).catch(err => {
console.warn("Not currently connected to Nostr extension:", err);
});
} else {
console.warn("Nostr extension not available");
}
try {
// Initialize NostrLogin without requiring UI elements
if (NostrLogin && NostrLogin.init) {
// Create a temporary container if needed
const tempContainer = document.createElement('div');
tempContainer.style.display = 'none';
document.body.appendChild(tempContainer);
console.log("Initializing NostrLogin...");
NostrLogin.init({
element: tempContainer,
onConnect: (pubkey: string): void => {
console.log(`Connected to Nostr with pubkey: ${pubkey.slice(0, 8)}...`);
// Store pubkey in localStorage for other parts of the app
localStorage.setItem('userPublicKey', pubkey);
// Update UI to show pubkey
updateClientPubkeyDisplay(pubkey);
},
onDisconnect: (): void => {
console.log('Disconnected from Nostr');
localStorage.removeItem('userPublicKey');
},
// Add error handler to catch "Already started" errors
onError: (error: Error): void => {
console.error("NostrLogin error:", error);
if (error.message === 'Already started') {
console.log("Detected 'Already started' error. Resetting state...");
// Clear auth state manually
window.sessionStorage.removeItem('nostrLoginInitialized');
['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
window.sessionStorage.removeItem(key);
});
}
}
});
// Mark as initialized to prevent duplicate initialization
window.sessionStorage.setItem('nostrLoginInitialized', 'true');
// Check if we can get an existing pubkey (already connected)
if (window.nostr) {
window.nostr.getPublicKey().then(pubkey => {
console.log(`Already connected with pubkey: ${pubkey.slice(0, 8)}...`);
localStorage.setItem('userPublicKey', pubkey);
updateClientPubkeyDisplay(pubkey);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Error initializing Nostr check: ${errorMessage}`);
}
}
/**
* Reset the Nostr login state in case of errors
* This can be used to recover from "Already started" errors
* This is simplified to use our stateless approach
*/
function resetNostrLoginState(): void {
console.log("Resetting Nostr login state...");
// Clear our initialization flags
window.sessionStorage.removeItem('nostrLoginInitialized');
// Clear any auth state that might be stuck
['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
window.sessionStorage.removeItem(key);
});
// Clear any relevant localStorage items
// (but keep the user's pubkey if they were logged in)
['nostrLoginTemporaryState'].forEach(key => {
localStorage.removeItem(key);
});
// If the NostrLogin library provides a reset method, use it
if (NostrLogin && typeof NostrLogin.reset === 'function') {
try {
NostrLogin.reset();
console.log("NostrLogin.reset() called successfully");
} catch (error) {
console.warn("Error calling NostrLogin.reset():", error);
}
}
// Reset the auth state in our service
authManager.clearAuthState();
console.log("Nostr login state has been reset. You can now try again.");
}
@ -688,74 +638,37 @@ function resetNostrLoginState(): void {
* @returns Promise resolving to whether the user is authenticated
*/
async function ensureAuthenticated(): Promise<boolean> {
// Check if we already have a pubkey in localStorage
const savedPubkey = localStorage.getItem('userPublicKey');
if (savedPubkey) {
console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`);
// Check directly with the Auth service, which checks the extension
const isAuth = await authManager.checkAuthentication();
if (isAuth) {
// Already authenticated
const pubkey = await authManager.getCurrentUserPubkeyAsync();
if (pubkey) {
// Update UI with the pubkey
updateClientPubkeyDisplay(pubkey);
}
return true;
}
// If we don't have a pubkey, check if nostr is available
// Not authenticated, check if extension is available
if (!window.nostr) {
console.warn("Nostr extension not available");
return false;
}
// Check if auth is already in progress
if (window.sessionStorage.getItem('nostrAuthInProgress') === 'true') {
console.log("Auth already in progress, resetting state first");
resetNostrLoginState();
// Wait a moment for the reset to take effect
await new Promise(resolve => setTimeout(resolve, 300));
}
// Set flag that we're starting auth
window.sessionStorage.setItem('nostrAuthInProgress', 'true');
try {
// Try to get the user's public key
console.log("Starting Nostr authentication...");
const pubkey = await window.nostr.getPublicKey();
const result = await authManager.loginWithNostrLogin();
if (pubkey) {
console.log(`Authentication successful, pubkey: ${pubkey.substring(0, 8)}...`);
// Store the pubkey for future use
localStorage.setItem('userPublicKey', pubkey);
// Update UI
updateClientPubkeyDisplay(pubkey);
return true;
}
return false;
return result !== null;
} catch (error) {
console.error("Error during authentication:", error);
// Handle "Already started" error specifically
if (error instanceof Error && error.message === 'Already started') {
console.log("Detected 'Already started' error. Resetting login state...");
resetNostrLoginState();
}
return false;
} finally {
// Clear the in-progress flag regardless of outcome
window.sessionStorage.removeItem('nostrAuthInProgress');
}
}
}).catch(err => {
console.warn("Not connected to Nostr extension:", err);
});
}
} else {
console.warn("NostrLogin initialization unavailable");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Error initializing Nostr login: ${errorMessage}`);
}
}
/**
* Handle the publish button click
@ -1127,244 +1040,100 @@ function handleToggleKeyFormat(): void {
}
});
}
/**
* Initialize authentication and ensure the user is signed in
* This should be called at startup to check authentication status
* Simple function to authenticate using direct Nostr extension check
*/
/**
* Fixed authentication flow that properly waits for completion
* This is a simpler approach that focuses on the specific issue
*/
async function safeAuthenticate(): Promise<string | null> {
console.log("Starting safe authentication...");
// Check if auth is already in progress to prevent duplicate attempts
if (window.authInProgress) {
console.log("Authentication already in progress, waiting...");
return new Promise((resolve) => {
// Set up a small interval to check when the auth is complete
const checkInterval = setInterval(() => {
if (!window.authInProgress) {
clearInterval(checkInterval);
// After auth is complete, check if we have a pubkey
const pubkey = localStorage.getItem('userPublicKey');
resolve(pubkey);
}
}, 100);
// Set a timeout in case the auth gets stuck
setTimeout(() => {
clearInterval(checkInterval);
console.log("Authentication wait timeout, proceeding...");
const pubkey = localStorage.getItem('userPublicKey');
resolve(pubkey);
}, 5000);
});
}
// Set the global auth flag to prevent duplicate attempts
window.authInProgress = true;
async function login(): Promise<string | null> {
try {
// Clear any stale auth state
window.sessionStorage.removeItem('nostrLoginInitialized');
['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
window.sessionStorage.removeItem(key);
});
// Use the auth service with direct extension check
const result = await authManager.authService.authenticate();
// Initialize NostrLogin
initNostrLogin();
// Wait a moment for initialization to complete
await new Promise(resolve => setTimeout(resolve, 300));
// Check if we're already authenticated via AuthenticationService
if (authManager.authService.isAuthenticated()) {
console.log("Already authenticated via AuthenticationService");
window.authInProgress = false;
const pubkey = authManager.getCurrentUserPubkey();
if (pubkey) {
updateClientPubkeyDisplay(pubkey);
return pubkey;
}
}
// If we already have a pubkey in localStorage, use that
const savedPubkey = localStorage.getItem('userPublicKey');
if (savedPubkey) {
console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`);
updateClientPubkeyDisplay(savedPubkey);
// Set authentication state in the central auth manager
authManager.setAuthenticated(true, savedPubkey);
window.authInProgress = false;
return savedPubkey;
}
} catch (error) {
console.error("Error in authentication setup:", error);
window.authInProgress = false;
return null;
}
// Set a flag that we're starting auth
window.sessionStorage.setItem('nostrAuthInProgress', 'true');
try {
// If the extension is available, try to get the pubkey
if (window.nostr) {
console.log("Requesting public key from extension...");
if (result.success && result.pubkey) {
const pubkey = result.pubkey;
console.log(`Authenticated with public key: ${pubkey.substring(0, 8)}...`);
// First try using our enhanced AuthenticationService
try {
const result = await authManager.authService.authenticate();
if (result.success && result.session) {
const pubkey = result.session.pubkey;
console.log(`Authentication successful via AuthenticationService, pubkey: ${pubkey.substring(0, 8)}...`);
localStorage.setItem('userPublicKey', pubkey);
updateClientPubkeyDisplay(pubkey);
window.authInProgress = false;
return pubkey;
}
} catch (authServiceError) {
console.warn("AuthenticationService authentication failed, falling back to extension:", authServiceError);
}
// Update UI to show pubkey
updateClientPubkeyDisplay(pubkey);
// Fall back to direct extension request
const pubkey = await window.nostr.getPublicKey();
if (pubkey) {
console.log(`Authentication successful via extension, pubkey: ${pubkey.substring(0, 8)}...`);
localStorage.setItem('userPublicKey', pubkey);
updateClientPubkeyDisplay(pubkey);
// Set authentication state in the central auth manager
authManager.setAuthenticated(true, pubkey);
window.authInProgress = false;
return pubkey;
}
} else {
console.warn("Nostr extension not available");
return pubkey;
}
window.authInProgress = false;
return null;
} catch (error) {
console.error("Error during authentication:", error);
window.authInProgress = false;
console.error('Login failed:', error);
return null;
} finally {
// Clear the in-progress flag
window.sessionStorage.removeItem('nostrAuthInProgress');
window.authInProgress = false;
}
}
// Set up auth state based on stored credentials but don't auto-authenticate
// This prevents automatic network requests on page load
(function() {
try {
const savedPubkey = localStorage.getItem('userPublicKey');
if (savedPubkey) {
console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`);
updateClientPubkeyDisplay(savedPubkey);
// Update the auth state but don't make network requests yet
authManager.updateAuthStateFromStorage();
}
} catch (error) {
console.error("Error checking stored authentication:", error);
}
})();
// Add a reset mechanism for auth state when navigating away from the page
// This helps ensure we don't get "Already started" errors on page reload
window.addEventListener('beforeunload', () => {
// Clear all auth-related state to ensure a clean start on the next page load
window.sessionStorage.removeItem('nostrLoginInitialized');
// Clear any temporary auth state in sessionStorage
// This is a preventative measure to avoid auth state conflicts
['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
window.sessionStorage.removeItem(key);
});
});
// Function to manually retry authentication if the user encounters issues
async function retryAuthentication(): Promise<void> {
console.log("Manual authentication retry requested");
// Check if auth is already in progress
if (window.authInProgress) {
console.log("Authentication already in progress, please wait...");
// If auth is in progress, just update UI
const statusElement = document.getElementById('authStatus');
if (statusElement) {
statusElement.textContent = 'Authentication already in progress...';
statusElement.className = 'status-message loading';
}
// Set a timeout to check if auth completed
setTimeout(() => {
if (authManager.isAuthenticated()) {
if (statusElement) {
statusElement.textContent = 'Authentication successful!';
statusElement.className = 'status-message success';
}
}
}, 5000);
return;
}
/**
* Function to login with nostr
* Uses direct extension check without localStorage or sessions
*/
async function loginWithNostr(): Promise<void> {
console.log("Nostr login requested");
// Clear UI status
const statusElement = document.getElementById('authStatus');
if (statusElement) {
statusElement.textContent = 'Retrying authentication...';
statusElement.textContent = 'Logging in...';
statusElement.className = 'status-message loading';
}
// Force reset auth state to clear any stuck processes
window.authInProgress = false;
window.sessionStorage.removeItem('nostrLoginInitialized');
['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
window.sessionStorage.removeItem(key);
});
// Wait a moment for state to clear
await new Promise(resolve => setTimeout(resolve, 300));
// Try authentication again with our enhanced service first
try {
// Use our direct extension check approach
const result = await authManager.authService.authenticate();
if (result.success && result.session) {
console.log(`Authentication successful via AuthenticationService: ${result.session.pubkey.substring(0, 8)}...`);
// Update UI status
if (statusElement) {
// Update UI based on result
if (statusElement) {
if (result.success && result.pubkey) {
statusElement.textContent = 'Authentication successful!';
statusElement.className = 'status-message success';
} else {
statusElement.textContent = result.error || 'Authentication failed. Please check your Nostr extension.';
statusElement.className = 'status-message error';
}
return;
}
} catch (error) {
console.warn("Could not authenticate with AuthenticationService, falling back:", error);
}
// Fall back to standard authentication
const pubkey = await safeAuthenticate();
// Update status
if (statusElement) {
if (pubkey) {
statusElement.textContent = 'Authentication successful!';
statusElement.className = 'status-message success';
} else {
console.error("Error during authentication:", error);
// Update UI status on error
if (statusElement) {
statusElement.textContent = 'Authentication failed. Please check your Nostr extension.';
statusElement.className = 'status-message error';
}
}
}
// Export login function for use in other modules
export { login, loginWithNostr };
// Set up auth state by checking with extension but don't auto-authenticate
// This prevents automatic network requests on page load
(function() {
try {
// Check if we're authenticated with the extension
authManager.getCurrentUserPubkeyAsync().then(pubkey => {
if (pubkey) {
console.log(`Found active pubkey: ${pubkey.substring(0, 8)}...`);
updateClientPubkeyDisplay(pubkey);
}
});
} catch (error) {
console.error("Error checking authentication status:", error);
}
})();
// Add a reset mechanism for auth state when navigating away from the page
// This helps ensure we don't get "Already started" errors on page reload
// No need to clear session storage on page unload anymore
// Since we're not using sessions for auth state
window.addEventListener('beforeunload', () => {
// Just clear any global state that might be problematic on reload
// but we no longer need to clear session storage
console.log('Page unloading, auth state will be re-checked on next load');
});
// Initialize HTTP response viewer with raw/formatted view support and 21121 integration
window.addEventListener('DOMContentLoaded', () => {
try {
@ -1563,7 +1332,7 @@ function handleToggleClientKeyFormat(): void {
try {
if (currentFormat === 'npub') {
// Switch to hex format
let hexValue = clientPubkeyElement.dataset.hex;
const hexValue = clientPubkeyElement.dataset.hex;
if (hexValue) {
clientPubkeyElement.textContent = hexValue;
@ -1573,7 +1342,7 @@ function handleToggleClientKeyFormat(): void {
}
} else {
// Switch to npub format
let npubValue = clientPubkeyElement.dataset.npub;
const npubValue = clientPubkeyElement.dataset.npub;
if (npubValue) {
clientPubkeyElement.textContent = npubValue;
@ -1615,7 +1384,7 @@ document.addEventListener('DOMContentLoaded', function(): void {
}
});
// Add auth status indicator and retry button to the UI
// Add sign in button or auth status based on authentication state
const addAuthUI = (): void => {
// Find a good place for the auth elements (navbar or header)
const navbarRight = document.querySelector('.nav-right, header');
@ -1628,37 +1397,59 @@ document.addEventListener('DOMContentLoaded', function(): void {
authContainer.style.marginLeft = 'auto';
authContainer.style.marginRight = '10px';
// Create status indicator
const authStatus = document.createElement('div');
authStatus.id = 'authStatus';
authStatus.className = 'status-message';
authStatus.style.fontSize = '0.8em';
authStatus.style.marginRight = '10px';
// Check current auth status
const pubkey = localStorage.getItem('userPublicKey');
if (pubkey) {
authStatus.textContent = 'Authenticated';
// User is authenticated, show status
const authStatus = document.createElement('div');
authStatus.id = 'authStatus';
authStatus.className = 'status-message success';
authStatus.style.fontSize = '0.8em';
authStatus.style.marginRight = '10px';
authStatus.textContent = 'Authenticated';
// Add sign out button
const signOutButton = document.createElement('button');
signOutButton.textContent = 'Sign Out';
signOutButton.className = 'auth-btn';
signOutButton.style.fontSize = '0.8em';
signOutButton.style.padding = '3px 8px';
signOutButton.addEventListener('click', () => {
authManager.clearAuthState();
localStorage.removeItem('userPublicKey');
window.location.reload(); // Reload to update UI
});
// Add elements to container
authContainer.appendChild(authStatus);
authContainer.appendChild(signOutButton);
} else {
authStatus.textContent = 'Not authenticated';
authStatus.className = 'status-message warning';
// User is not authenticated, show sign in button
const signInButton = document.createElement('button');
signInButton.textContent = 'Sign in with Nostr';
signInButton.className = 'auth-btn sign-in-btn';
signInButton.style.fontSize = '0.8em';
signInButton.style.padding = '3px 8px';
signInButton.style.backgroundColor = 'var(--button-primary, #4285f4)';
signInButton.style.color = 'white';
signInButton.style.border = 'none';
signInButton.style.borderRadius = '4px';
signInButton.style.cursor = 'pointer';
signInButton.addEventListener('click', async () => {
await loginWithNostr();
// Reload the page after authentication attempt to update UI
if (authManager.isAuthenticated()) {
window.location.reload();
}
});
// Add sign in button to container
authContainer.appendChild(signInButton);
}
// Create retry button
const retryButton = document.createElement('button');
retryButton.textContent = 'Retry Auth';
retryButton.className = 'auth-retry-btn';
retryButton.style.fontSize = '0.8em';
retryButton.style.padding = '3px 8px';
retryButton.addEventListener('click', async () => {
await retryAuthentication();
});
// Add elements to container
authContainer.appendChild(authStatus);
authContainer.appendChild(retryButton);
// Add container to navbar
navbarRight.appendChild(authContainer);
}
@ -1700,7 +1491,7 @@ document.addEventListener('DOMContentLoaded', function(): void {
}
// Attempt authentication
await retryAuthentication();
await loginWithNostr();
// If authentication succeeded, proceed with search
if (authManager.isAuthenticated()) {
@ -1740,7 +1531,7 @@ document.addEventListener('DOMContentLoaded', function(): void {
}
// Attempt authentication
await retryAuthentication();
await loginWithNostr();
}
if (authManager.isAuthenticated()) {

@ -5,12 +5,15 @@
* in a table format with a detailed view modal.
*/
import { EventChangeType } from '../services/EventManager';
import type { ClientEventStore, ClientStoredEvent } from '../services/ClientEventStore';
import { ClientEventStatus } from '../services/ClientEventStore';
import { HttpFormatter } from '../services/HttpFormatter';
import { nip19 } from 'nostr-tools';
import { reconnectRelayService } from '../client-event-handler';
import type { ClientEventStore } from '../services/ClientEventStore';
import { ClientEventStatus } from '../services/ClientEventStore';
import { EventChangeType } from '../services/EventManager';
import { HttpFormatter } from '../services/HttpFormatter';
export class ClientEventsTable {
private container: HTMLElement | null = null;
@ -270,7 +273,7 @@ export class ClientEventsTable {
try {
const npub = nip19.npubEncode(targetServerTag[1]);
targetServer = `${npub.substring(0, 8)}...${npub.substring(npub.length - 4)}`;
} catch (e) {
} catch {
targetServer = targetServerTag[1].substring(0, 8) + '...';
}
}
@ -279,7 +282,7 @@ export class ClientEventsTable {
const shortEventId = eventId.substring(0, 8) + '...';
// Determine status indicator
let statusHtml = this.getStatusHtml(storedEvent.status);
const statusHtml = this.getStatusHtml(storedEvent.status);
// Set row HTML
row.innerHTML = `
@ -371,15 +374,19 @@ export class ClientEventsTable {
private loadEventData(eventId: string): void {
const storedEvent = this.eventStore.getEvent(eventId);
if (!storedEvent || !this.modal) return;
if (!storedEvent || !this.modal) {
return;
}
// Get tab content containers
const jsonTab = document.getElementById('tab-21120-json-content');
const httpRequestTab = document.getElementById('tab-21120-http-content');
const httpResponseTab = document.getElementById('tab-21121-http-content');
const jsonResponseTab = document.getElementById('tab-21121-json-content');
if (!jsonTab || !httpRequestTab || !httpResponseTab || !jsonResponseTab) {
return;
}
if (!jsonTab || !httpRequestTab || !httpResponseTab || !jsonResponseTab) return;
// Populate 21120 JSON tab
jsonTab.innerHTML = `<pre>${JSON.stringify(storedEvent.event, null, 2)}</pre>`;
@ -387,7 +394,7 @@ export class ClientEventsTable {
// Populate 21120 HTTP request tab
try {
httpRequestTab.innerHTML = HttpFormatter.formatHttpContent(storedEvent.event.content, true, true);
} catch (e) {
} catch {
httpRequestTab.innerHTML = `<pre>${storedEvent.event.content}</pre>`;
}
@ -407,7 +414,7 @@ export class ClientEventsTable {
// Populate 21121 HTTP response tab
try {
httpResponseTab.innerHTML = HttpFormatter.formatHttpContent(responseEvent.content, false, true);
} catch (e) {
} catch {
httpResponseTab.innerHTML = `<pre>${responseEvent.content}</pre>`;
}
} else {
@ -517,7 +524,9 @@ export class ClientEventsTable {
}
private showModal(eventId: string): void {
if (!this.modal) return;
if (!this.modal) {
return;
}
this.currentEventId = eventId;
@ -535,7 +544,9 @@ export class ClientEventsTable {
}
private hideModal(): void {
if (!this.modal) return;
if (!this.modal) {
return;
}
this.modal.style.display = 'none';
this.currentEventId = null;
@ -543,7 +554,9 @@ export class ClientEventsTable {
private switchTab(event: Event): void {
const clickedTab = event.currentTarget as HTMLElement;
if (!clickedTab || !clickedTab.dataset.tabId) return;
if (!clickedTab || !clickedTab.dataset.tabId) {
return;
}
// Hide all tab contents
const tabContents = document.querySelectorAll('.modal-tab-content');

@ -4,7 +4,8 @@
*/
import { NostrEvent } from '../relay';
import { EventManager, EventChangeType } from '../services/EventManager';
import type { EventManager} from '../services/EventManager';
import { EventChangeType } from '../services/EventManager';
import { HttpFormatter } from '../services/HttpFormatter';
/**
@ -92,7 +93,7 @@ export class EventDetail {
* Show empty state when no event is selected
*/
private showEmptyState(): void {
if (!this.container) return;
if (!this.container) {return;}
this.container.innerHTML = `
<div class="empty-state">
@ -105,7 +106,7 @@ export class EventDetail {
* Render the details of the currently selected event
*/
private renderEventDetail(): void {
if (!this.container) return;
if (!this.container) {return;}
// Get the selected event from the EventManager
const managedEvent = this.eventManager.getSelectedEvent();
@ -121,7 +122,7 @@ export class EventDetail {
const isResponse = event.kind === 21121;
// Determine the content to display
let httpContent = managedEvent.decrypted ?
const httpContent = managedEvent.decrypted ?
managedEvent.decryptedContent || event.content :
event.content;
@ -223,7 +224,7 @@ export class EventDetail {
* Set up tab buttons for switching between raw and formatted views
*/
private setupTabButtons(): void {
if (!this.container) return;
if (!this.container) {return;}
const tabButtons = this.container.querySelectorAll('.tab-btn');
tabButtons.forEach(button => {
@ -251,7 +252,7 @@ export class EventDetail {
* Set up event listeners for related events and action buttons
*/
private setupEventListeners(): void {
if (!this.container) return;
if (!this.container) {return;}
// Related event links
const relatedLinks = this.container.querySelectorAll('.related-event-link');

@ -3,8 +3,9 @@
* Modular UI component for rendering and managing a list of Nostr events
*/
import { NostrEvent } from '../relay';
import { EventManager, EventChangeType } from '../services/EventManager';
import type { NostrEvent } from '../relay';
import type { EventManager} from '../services/EventManager';
import { EventChangeType } from '../services/EventManager';
/**
* Options for initializing the EventList component
@ -109,7 +110,7 @@ export class EventList {
* Create the UI structure for the event list and controls
*/
private createUIStructure(): void {
if (!this.container) return;
if (!this.container) {return;}
// Create the header with enhanced search and filter controls
const header = document.createElement('div');
@ -343,7 +344,7 @@ export class EventList {
private saveCurrentFilter(): void {
// Create a dialog to name the filter
const filterName = prompt('Enter a name for this filter:', 'Filter ' + (this.savedFilters.length + 1));
if (!filterName) return;
if (!filterName) {return;}
// Save current filter configuration
const filterConfig = {
@ -370,7 +371,7 @@ export class EventList {
*/
private updateSavedFiltersList(): void {
const savedFiltersList = document.getElementById('savedFiltersList');
if (!savedFiltersList) return;
if (!savedFiltersList) {return;}
if (this.savedFilters.length === 0) {
savedFiltersList.innerHTML = '<div class="empty-saved-filters">No saved filters</div>';
@ -409,7 +410,7 @@ export class EventList {
* Apply a saved filter
*/
private applyFilter(index: number): void {
if (index < 0 || index >= this.savedFilters.length) return;
if (index < 0 || index >= this.savedFilters.length) {return;}
const filter = this.savedFilters[index].config;
@ -450,7 +451,7 @@ export class EventList {
* Delete a saved filter
*/
private deleteFilter(index: number): void {
if (index < 0 || index >= this.savedFilters.length) return;
if (index < 0 || index >= this.savedFilters.length) {return;}
// Remove the filter
this.savedFilters.splice(index, 1);
@ -478,7 +479,7 @@ export class EventList {
*/
private setupVirtualScrolling(): void {
const eventsListWrapper = this.eventsListContainer?.parentElement;
if (!eventsListWrapper) return;
if (!eventsListWrapper) {return;}
// Create intersection observer to detect visible events
this.scrollObserver = new IntersectionObserver(
@ -516,7 +517,7 @@ export class EventList {
* Render all existing events from the EventManager
*/
private renderExistingEvents(): void {
if (!this.eventsListContainer) return;
if (!this.eventsListContainer) {return;}
// Clear existing content and tracked events
this.eventsListContainer.innerHTML = '';
@ -537,7 +538,7 @@ export class EventList {
this.filteredEventIds.sort((a, b) => {
const eventA = this.eventManager.getEvent(a);
const eventB = this.eventManager.getEvent(b);
if (!eventA || !eventB) return 0;
if (!eventA || !eventB) {return 0;}
return eventB.receivedAt - eventA.receivedAt;
});
@ -555,7 +556,7 @@ export class EventList {
* Update which events are visible in the virtual scroll view
*/
private updateVisibleEvents(): void {
if (!this.eventsListContainer || !this.eventsListContainer.parentElement) return;
if (!this.eventsListContainer || !this.eventsListContainer.parentElement) {return;}
const scrollContainer = this.eventsListContainer.parentElement;
const scrollTop = scrollContainer.scrollTop;
@ -616,14 +617,14 @@ export class EventList {
// Apply filters to get filtered IDs
this.filteredEventIds = this.allEventIds.filter(eventId => {
const managedEvent = this.eventManager.getEvent(eventId);
if (!managedEvent) return false;
if (!managedEvent) {return false;}
const event = managedEvent.event;
// Filter by server if needed
if (!this.showAllEvents) {
const isToServer = this.checkIfToServer(event);
if (!isToServer) return false;
if (!isToServer) {return false;}
}
// Filter by event type
@ -680,7 +681,7 @@ export class EventList {
this.filteredEventIds.sort((a, b) => {
const eventA = this.eventManager.getEvent(a);
const eventB = this.eventManager.getEvent(b);
if (!eventA || !eventB) return 0;
if (!eventA || !eventB) {return 0;}
return eventB.receivedAt - eventA.receivedAt;
});
@ -701,7 +702,7 @@ export class EventList {
// Legacy code for backward compatibility
const eventsList = document.getElementById('eventsList');
if (!eventsList) return;
if (!eventsList) {return;}
const items = eventsList.querySelectorAll('.event-item');
let visibleCount = 0;
@ -739,7 +740,7 @@ export class EventList {
* Show empty state when there are no events
*/
private showEmptyState(): void {
if (!this.eventsListContainer) return;
if (!this.eventsListContainer) {return;}
// Clear virtual scrolling data
this.filteredEventIds = [];
@ -762,11 +763,11 @@ export class EventList {
private checkIfToServer(event: NostrEvent): boolean {
// Get server pubkey from EventManager
const serverPubkey = this.eventManager.getServerPubkey();
if (!serverPubkey) return false;
if (!serverPubkey) {return false;}
// Check for p tag to identify recipient
const pTag = event.tags.find(tag => tag[0] === 'p');
if (!pTag || pTag.length <= 1) return false;
if (!pTag || pTag.length <= 1) {return false;}
// Check if the p tag matches our server pubkey
return (pTag[1] === serverPubkey);
@ -778,7 +779,7 @@ export class EventList {
private getRecipientDisplay(event: NostrEvent): string {
// Find recipient if any
const pTag = event.tags.find(tag => tag[0] === 'p');
if (!pTag || pTag.length <= 1) return '';
if (!pTag || pTag.length <= 1) {return '';}
return `<div class="recipient">To: ${pTag[1].substring(0, 8)}...</div>`;
}
@ -788,11 +789,11 @@ export class EventList {
*/
private renderEventItem(eventId: string): HTMLElement | null {
const eventsList = document.getElementById('eventsList');
if (!eventsList) return null;
if (!eventsList) {return null;}
// Get the event from EventManager
const managedEvent = this.eventManager.getEvent(eventId);
if (!managedEvent) return null;
if (!managedEvent) {return null;}
const event = managedEvent.event;
@ -861,10 +862,10 @@ export class EventList {
const statusCode = statusMatch[1];
let statusClass = '';
if (statusCode.startsWith('2')) statusClass = 'status-success';
else if (statusCode.startsWith('3')) statusClass = 'status-redirect';
else if (statusCode.startsWith('4')) statusClass = 'status-client-error';
else if (statusCode.startsWith('5')) statusClass = 'status-server-error';
if (statusCode.startsWith('2')) {statusClass = 'status-success';}
else if (statusCode.startsWith('3')) {statusClass = 'status-redirect';}
else if (statusCode.startsWith('4')) {statusClass = 'status-client-error';}
else if (statusCode.startsWith('5')) {statusClass = 'status-server-error';}
httpContext = `<div class="http-preview ${statusClass}">Status: ${statusCode}</div>`;
}
@ -1094,7 +1095,7 @@ export class EventList {
* Highlight the selected event in the UI
*/
private highlightSelectedEvent(eventId: string): void {
if (!this.eventsListContainer || !this.eventsListContainer.parentElement) return;
if (!this.eventsListContainer || !this.eventsListContainer.parentElement) {return;}
// 1. Remove selected class from all visible items
this.visibleEvents.forEach((element) => {
@ -1172,7 +1173,7 @@ export class EventList {
private displayResponsesForRequest(requestId: string): void {
// Find the responses list container
const responsesListContainer = document.getElementById('responsesList');
if (!responsesListContainer) return;
if (!responsesListContainer) {return;}
// Get related response events from EventManager
const responses = this.eventManager.getResponsesForRequest(requestId);
@ -1210,10 +1211,10 @@ export class EventList {
const statusCode = statusMatch[1];
let statusClass = '';
if (statusCode.startsWith('2')) statusClass = 'status-success';
else if (statusCode.startsWith('3')) statusClass = 'status-redirect';
else if (statusCode.startsWith('4')) statusClass = 'status-client-error';
else if (statusCode.startsWith('5')) statusClass = 'status-server-error';
if (statusCode.startsWith('2')) {statusClass = 'status-success';}
else if (statusCode.startsWith('3')) {statusClass = 'status-redirect';}
else if (statusCode.startsWith('4')) {statusClass = 'status-client-error';}
else if (statusCode.startsWith('5')) {statusClass = 'status-server-error';}
statusInfo = `<span class="status-code ${statusClass}">Status: ${statusCode}</span>`;
}
@ -1253,14 +1254,14 @@ export class EventList {
*/
private displayResponseJson(event: NostrEvent): void {
const jsonContainer = document.getElementById('response21121Json');
if (!jsonContainer) return;
if (!jsonContainer) {return;}
// Make container visible
jsonContainer.style.display = 'block';
// Get the pre element
const pre = jsonContainer.querySelector('pre.json-content');
if (!pre) return;
if (!pre) {return;}
// Format JSON with indentation
pre.textContent = JSON.stringify(event, null, 2);

@ -5,9 +5,10 @@
* Includes a modal dialog to show detailed information when a row is clicked
*/
import { nip19 } from 'nostr-tools';
import { EventChangeType, EventKind } from '../services/EventManager';
import type { EventManager } from '../services/EventManager';
import { nip19 } from 'nostr-tools';
import { HttpFormatter } from '../services/HttpFormatter';
export class HttpMessagesTable {
@ -302,7 +303,7 @@ export class HttpMessagesTable {
* @param eventId The ID of the event to show details for
*/
private showModal(eventId: string): void {
if (!this.modal) return;
if (!this.modal) {return;}
this.currentEventId = eventId;
@ -323,7 +324,7 @@ export class HttpMessagesTable {
* Hide the modal dialog
*/
private hideModal(): void {
if (!this.modal) return;
if (!this.modal) {return;}
this.modal.style.display = 'none';
this.currentEventId = null;
@ -335,7 +336,7 @@ export class HttpMessagesTable {
*/
private switchTab(event: Event): void {
const clickedTab = event.currentTarget as HTMLElement;
if (!clickedTab || !clickedTab.dataset.tabId) return;
if (!clickedTab || !clickedTab.dataset.tabId) {return;}
// Hide all tab contents
const tabContents = document.querySelectorAll('.modal-tab-content');
@ -565,7 +566,7 @@ export class HttpMessagesTable {
* This can be called when a new 21121 response is received
*/
public updateResponseIndicators(): void {
if (!this.tableBody) return;
if (!this.tableBody) {return;}
const rows = this.tableBody.querySelectorAll('.event-row');
rows.forEach(row => {

@ -6,9 +6,9 @@
*/
import { NostrEvent } from '../relay';
import { HttpClient } from '../services/HttpClient';
import type { EventManager } from '../services/EventManager';
import type { HttpClient } from '../services/HttpClient';
import { ToastNotifier } from '../services/ToastNotifier';
import { EventManager } from '../services/EventManager';
// Result of an HTTP request execution
export interface ExecutionResult {

@ -9,9 +9,10 @@
* - Publishing to relays
*/
import { NostrEvent } from '../relay';
import * as nostrTools from 'nostr-tools';
import { EventManager } from '../services/EventManager';
import type { NostrEvent } from '../relay';
import type { EventManager } from '../services/EventManager';
import { ToastNotifier } from '../services/ToastNotifier';
/**
@ -185,7 +186,7 @@ export class Nostr21121Creator {
const pubKey = nostrTools.getPublicKey(privateKeyBytes);
// Initialize tags array
let tags: string[][] = [];
const tags: string[][] = [];
// Always add reference to the request event
if (requestEvent.id) {
@ -197,7 +198,7 @@ export class Nostr21121Creator {
// Check if the original event has a p tag (recipient)
const pTag = requestEvent.tags.find(tag => tag[0] === 'p');
let finalContent = responseContent;
const finalContent = responseContent;
if (pTag && pTag[1]) {
// Add p tag to reference the recipient

@ -3,10 +3,11 @@
* Handles HTTP response display and 21121 response event creation
*/
import { EventManager } from '../services/EventManager';
import type { EventManager } from '../services/EventManager';
import { HttpFormatter } from '../services/HttpFormatter';
import { ToastNotifier } from '../services/ToastNotifier';
import { ExecutionResult } from './HttpRequestExecutor';
import type { ExecutionResult } from './HttpRequestExecutor';
import { Nostr21121Creator, Creation21121Result } from './Nostr21121Creator';
/**
@ -163,7 +164,7 @@ export class ResponseViewer {
* Add a button to create a 21121 response event if not already present
*/
private addCreate21121Button(): void {
if (!this.modalElement) return;
if (!this.modalElement) {return;}
// Create or update status element
if (!this.creationStatus) {
@ -180,11 +181,11 @@ export class ResponseViewer {
// Check if button already exists
const existingButton = this.modalElement.querySelector('.create-21121-btn');
if (existingButton) return;
if (existingButton) {return;}
// Get the modal header
const modalHeader = this.modalElement.querySelector('.http-response-header');
if (!modalHeader) return;
if (!modalHeader) {return;}
// Create button
const button = document.createElement('button');
@ -205,7 +206,7 @@ export class ResponseViewer {
* Set up event listeners for modal interactions
*/
private setupModalEventListeners(): void {
if (!this.modalElement) return;
if (!this.modalElement) {return;}
// Handle close button click
const closeBtn = this.modalElement.querySelector('.close-modal-btn');
@ -223,7 +224,7 @@ export class ResponseViewer {
button.addEventListener('click', () => {
// Get the tab ID
const tabId = (button as HTMLElement).dataset.tab;
if (!tabId) return;
if (!tabId) {return;}
// Remove active class from all buttons and content
tabButtons.forEach(btn => btn.classList.remove('active'));
@ -272,7 +273,7 @@ export class ResponseViewer {
if (hasResponse) {
const shouldOverwrite = confirm('A response already exists for this request. Create another one?');
if (!shouldOverwrite) return;
if (!shouldOverwrite) {return;}
}
// Generate a sample response
@ -312,7 +313,7 @@ export class ResponseViewer {
* Show a dialog with options for creating a 21121 response
*/
private showCreateResponseDialog(requestEventId: string, responseContent: string): void {
if (!this.modalElement || !this.creationStatus) return;
if (!this.modalElement || !this.creationStatus) {return;}
// Update and show the status element
this.creationStatus.className = 'creation-status info';

@ -4,19 +4,21 @@
*/
import { EventManager } from '../services/EventManager';
import { HttpClient } from '../services/HttpClient';
import { HttpService } from '../services/HttpService';
import { Nostr21121EventHandler } from '../services/Nostr21121EventHandler';
import { NostrCacheService } from '../services/NostrCacheService';
import { NostrEventService } from '../services/NostrEventService.updated';
import { NostrRelayService } from '../services/NostrRelayService';
import { NostrCacheService } from '../services/NostrCacheService';
import { HttpService } from '../services/HttpService';
import { HttpClient } from '../services/HttpClient';
import { NostrService } from '../services/NostrService';
import { ToastNotifier } from '../services/ToastNotifier';
import { EventDetail } from './EventDetail';
import { EventList } from './EventList';
import { HttpMessagesTable } from './HttpMessagesTable';
import { HttpRequestExecutor } from './HttpRequestExecutor';
import { ResponseViewer } from './ResponseViewer';
import { EventList } from './EventList';
import { EventDetail } from './EventDetail';
import { HttpMessagesTable } from './HttpMessagesTable';
import { Nostr21121EventHandler } from '../services/Nostr21121EventHandler';
import { NostrService } from '../services/NostrService';
/**
@ -232,7 +234,7 @@ export class ServerUI {
*/
private connectToRelay(): void {
const relayUrlInput = document.getElementById(this.options.relayUrlInput) as HTMLInputElement;
if (!relayUrlInput) return;
if (!relayUrlInput) {return;}
const relayUrl = relayUrlInput.value.trim() || 'wss://relay.degmods.com';
if (!relayUrl) {

@ -31,7 +31,7 @@ declare global {
}
// Import nostr-login for encryption/signing
// eslint-disable-next-line no-undef
// Define better types for the NostrLogin module
interface NostrLoginModule {
signEvent: (event: NostrEvent) => Promise<NostrEvent>;

@ -19,7 +19,7 @@ export function initHttpResponseViewer(): void {
// Handle tab switching
if (target && target.classList.contains('tab-btn')) {
const tabContainer = target.closest('.http-response-tabs, .event-detail-tabs');
if (!tabContainer) return;
if (!tabContainer) {return;}
// Get all tab buttons and content in this container
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
@ -31,7 +31,7 @@ export function initHttpResponseViewer(): void {
tabContentContainer = tabContainer.closest('.modal-content, .event-details');
}
if (!tabContentContainer) return;
if (!tabContentContainer) {return;}
const tabContents = tabContentContainer.querySelectorAll('.tab-content');
@ -100,24 +100,24 @@ async function executeHttpRequest(button: HTMLElement): Promise<void> {
try {
// Find the HTTP content
const eventDetails = button.closest('.event-details');
if (!eventDetails) throw new Error('Event details not found');
if (!eventDetails) {throw new Error('Event details not found');}
// Find the HTTP content element - look in both formatted and raw tabs
const httpContentElement =
eventDetails.querySelector('#raw-http .http-content') ||
eventDetails.querySelector('.http-content');
if (!httpContentElement) throw new Error('HTTP content not found');
if (!httpContentElement) {throw new Error('HTTP content not found');}
const httpContent = httpContentElement.textContent || '';
if (!httpContent.trim()) throw new Error('Empty HTTP content');
if (!httpContent.trim()) {throw new Error('Empty HTTP content');}
// Get the event ID
const headerElement = eventDetails.querySelector('.event-detail-header h3');
const eventIdMatch = headerElement?.textContent?.match(/ID: (\w+)\.\.\./);
const eventId = eventIdMatch ? eventIdMatch[1] : null;
if (!eventId) throw new Error('Could not determine event ID');
if (!eventId) {throw new Error('Could not determine event ID');}
// Execute the HTTP request
const response = await executeRequest(httpContent);
@ -161,7 +161,7 @@ async function executeRequest(requestContent: string): Promise<string> {
// Extract the host
let host = '';
let headers: Record<string, string> = {};
const headers: Record<string, string> = {};
let body = '';
let inHeaders = true;

@ -3,12 +3,13 @@
* Handles displaying HTTP responses and 21121 integration
* Refactored to use EventManager for centralized event data management
*/
import { NostrEvent } from './relay';
import { HttpFormatter } from './services/HttpFormatter';
import { ToastNotifier } from './services/ToastNotifier';
import { EventManager, EventKind, EventChangeType } from './services/EventManager';
import { HttpService } from './services/HttpService';
import type { NostrEvent } from './relay';
import type { EventManager} from './services/EventManager';
import { EventKind, EventChangeType } from './services/EventManager';
import { HttpClient } from './services/HttpClient';
import { HttpFormatter } from './services/HttpFormatter';
import { HttpService } from './services/HttpService';
import { ToastNotifier } from './services/ToastNotifier';
// Services that will be dynamically imported to avoid circular dependencies
let nostrService: any = null;
@ -36,7 +37,7 @@ export function initHttpResponseViewer(
// Handle tab switching
if (target && target.classList.contains('tab-btn')) {
const tabContainer = target.closest('.http-response-tabs, .event-detail-tabs');
if (!tabContainer) return;
if (!tabContainer) {return;}
// Get all tab buttons and content in this container
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
@ -48,7 +49,7 @@ export function initHttpResponseViewer(
tabContentContainer = tabContainer.closest('.modal-content, .event-details');
}
if (!tabContentContainer) return;
if (!tabContentContainer) {return;}
const tabContents = tabContentContainer.querySelectorAll('.tab-content');

@ -1,148 +0,0 @@
/**
* navbar-diagnostics.ts
* Diagnostic tool to help debug navbar issues
*/
// Simple IIFE to run diagnostics and attempt to fix navbar issues
(function() {
// Create a diagnostic function that's accessible from the console
function runNavbarDiagnostics() {
console.log('%c[NAVBAR DIAGNOSTICS]', 'background: #ff0000; color: white; padding: 5px; font-size: 16px;');
// Check if the container exists
const navbarContainer = document.getElementById('navbarContainer');
console.log('1. Navbar container exists:', !!navbarContainer);
// If no container, this is a critical issue
if (!navbarContainer) {
console.error('CRITICAL: Navbar container not found!');
console.log('Attempting to create navbar container...');
// Create a new navbar container
const newContainer = document.createElement('div');
newContainer.id = 'navbarContainer';
newContainer.className = 'top-nav';
// Insert it at the top of the body
document.body.insertBefore(newContainer, document.body.firstChild);
console.log('Created new navbar container:', !!document.getElementById('navbarContainer'));
}
// Check if navbarContainer has content
const navbarContent = document.querySelector('.nav-left, .nav-right');
console.log('2. Navbar has content:', !!navbarContent);
if (!navbarContent) {
console.log('Navbar has no content. Attempting to fix...');
// Try to initialize the navbar
if (window.hasOwnProperty('initializeNavbar')) {
console.log('Calling initializeNavbar function...');
try {
(window as any).initializeNavbar();
} catch (error) {
console.error('Error calling initializeNavbar:', error);
}
} else {
console.error('initializeNavbar function not found globally!');
// Try to manually import and call
console.log('Attempting manual import...');
try {
// Get the current page
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
// Create emergency navbar content
const emergencyNavbar = document.getElementById('navbarContainer');
if (emergencyNavbar) {
const navbarHtml = `
<div class="nav-left">
<a href="./index.html" class="nav-link${currentPage === 'index.html' ? ' active' : ''}">HOME</a>
<a href="./1120_client.html" class="nav-link${currentPage === '1120_client.html' ? ' active' : ''}">CLIENT</a>
<a href="./1120_server.html" class="nav-link${currentPage === '1120_server.html' ? ' active' : ''}">SERVER</a>
<a href="./billboard.html" class="nav-link${currentPage === 'billboard.html' ? ' active' : ''}">BILLBOARD</a>
</div>
<div class="nav-right">
<a href="./index.html" class="nav-link nav-icon${currentPage === 'index.html' ? ' active' : ''}" title="Documentation"></a>
<a href="./profile.html" class="nav-link nav-icon${currentPage === 'profile.html' ? ' active' : ''}" title="Profile">👤</a>
<button id="nuclearResetBtn" class="nuclear-reset-btn" title="Reset All Data">💣</button>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
<span id="themeIcon">🌙</span>
</button>
</div>`;
emergencyNavbar.innerHTML = navbarHtml;
console.log('Added emergency navbar HTML!');
}
} catch (error) {
console.error('Error creating emergency navbar:', error);
}
}
}
// Check CSS visibility issues
const navbar = document.getElementById('navbarContainer');
if (navbar) {
const styles = window.getComputedStyle(navbar);
console.log('3. Navbar CSS visibility check:');
console.log(' - display:', styles.display);
console.log(' - visibility:', styles.visibility);
console.log(' - opacity:', styles.opacity);
console.log(' - height:', styles.height);
// Fix any CSS issues
if (styles.display === 'none') {
console.log('Fixing display:none issue...');
navbar.style.display = 'flex';
}
if (styles.visibility === 'hidden') {
console.log('Fixing visibility:hidden issue...');
navbar.style.visibility = 'visible';
}
if (styles.opacity === '0') {
console.log('Fixing opacity:0 issue...');
navbar.style.opacity = '1';
}
if (parseFloat(styles.height) === 0) {
console.log('Fixing zero height issue...');
navbar.style.height = 'auto';
}
}
// Final check
const navbarContentAfterFix = document.querySelector('.nav-left, .nav-right');
console.log('4. Navbar fixed successfully:', !!navbarContentAfterFix);
// Return result of diagnostics
return {
containerExists: !!navbarContainer,
hasContent: !!navbarContentAfterFix
};
}
// Make the diagnostics function globally available
if (typeof window !== 'undefined') {
(window as any).runNavbarDiagnostics = runNavbarDiagnostics;
}
// Run diagnostics on page load
window.addEventListener('load', function() {
console.log('Running navbar diagnostics on window.load...');
setTimeout(runNavbarDiagnostics, 1000);
});
// Also run it after DOM content is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
console.log('Running navbar diagnostics on DOMContentLoaded...');
runNavbarDiagnostics();
});
} else {
// If already loaded, run it now
console.log('Document already loaded, running navbar diagnostics immediately...');
runNavbarDiagnostics();
}
})();
// Export a dummy function so TypeScript treats this as a module
export function navbarDiagnostics(): void {
console.log('Navbar diagnostics module loaded');
}

@ -1,8 +1,6 @@
// External dependencies
import * as nostrTools from 'nostr-tools';
import qrcode from 'qrcode-generator';
import * as authManager from './auth-manager';
import { SecureStorageService } from './services/SecureStorageService';
// Interface for profile data
interface ProfileData {
@ -10,7 +8,8 @@ interface ProfileData {
about?: string;
picture?: string;
nip05?: string;
[key: string]: any;
// Allow additional string properties for Nostr metadata
[key: string]: string | undefined;
}
// Element references
@ -37,7 +36,7 @@ let currentProfileData: ProfileData = {
nip05: ''
};
// Connect to extension
// Connect with Nostr extension directly
async function connectWithExtension(): Promise<string | null> {
try {
if (!window.nostr) {
@ -45,54 +44,36 @@ async function connectWithExtension(): Promise<string | null> {
return null;
}
// Use the AuthenticationService via auth-manager for a more secure login
const result = await authManager.authService.authenticate();
if (result.success && result.session) {
currentPubkey = result.session.pubkey;
// Log the successful authentication
console.log(`Authentication successful using secure method for pubkey: ${currentPubkey.substring(0, 8)}...`);
updateConnectionStatus(`Connected with extension using pubkey: ${currentPubkey.substring(0, 8)}...`, true);
showProfile(currentPubkey);
// Return the pubkey so it can be used by the caller
return currentPubkey;
} else {
// Fall back to the old method if needed
console.log('Secure authentication failed, trying legacy method');
// Use the Nostr extension directly
try {
const pubkey = await window.nostr.getPublicKey();
if (pubkey) {
currentPubkey = pubkey;
// Set the authenticated state using auth-manager
authManager.setAuthenticated(true, pubkey);
console.log(`Authentication successful (legacy) for pubkey: ${pubkey.substring(0, 8)}...`);
// Log the successful authentication
console.log(`Authentication successful using Nostr extension for pubkey: ${currentPubkey.substring(0, 8)}...`);
updateConnectionStatus(`Connected with extension using pubkey: ${pubkey.substring(0, 8)}...`, true);
showProfile(pubkey);
updateConnectionStatus(`Connected with extension using pubkey: ${currentPubkey.substring(0, 8)}...`, true);
showProfile(currentPubkey);
// Return the pubkey so it can be used by the caller
return pubkey;
return currentPubkey;
} else {
authManager.setAuthenticated(false);
updateConnectionStatus('Failed to get public key from extension', false);
// Authentication failed
updateConnectionStatus(`Authentication failed: Could not get pubkey from extension`, false);
return null;
}
} catch (error) {
updateConnectionStatus(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`, false);
return null;
}
} catch (err) {
console.error('Error connecting with extension:', err);
// Make sure we set authentication to false on error
authManager.setAuthenticated(false);
updateConnectionStatus(`Error: ${err instanceof Error ? err.message : String(err)}`, false);
return null;
}
}
// Manual pubkey functionality removed as users are automatically logged in
// Update connection status UI
function updateConnectionStatus(message: string, isConnected: boolean): void {
if (!connectionStatus) {return;}
@ -216,7 +197,7 @@ async function fetchProfileData(pubkey: string): Promise<void> {
if (typeof msg.data !== 'string') {return;}
try {
const data = JSON.parse(msg.data);
const data = JSON.parse(msg.data) as [string, string, Record<string, unknown>];
if (Array.isArray(data) && data[0] === 'EVENT' && data[1] === requestId) {
events.push(data[2]);
connected = true;
@ -324,7 +305,6 @@ function updateStats(): void {
if (!statsRequestsSent || !statsResponsesReceived || !statsRelaysConnected) {return;}
// For this demo, just set some placeholder values
// In a real app, you would track these stats in localStorage or a database
statsRequestsSent.textContent = '0';
statsResponsesReceived.textContent = '0';
statsRelaysConnected.textContent = '0';
@ -371,8 +351,6 @@ document.addEventListener('DOMContentLoaded', async () => {
statsResponsesReceived = document.getElementById('responsesReceived');
statsRelaysConnected = document.getElementById('relaysConnected');
// Auto-connect, no manual button needed
// Copy npub button
const copyNpubBtn = document.getElementById('copyNpubBtn');
if (copyNpubBtn) {
@ -385,39 +363,19 @@ document.addEventListener('DOMContentLoaded', async () => {
refreshProfileBtn.addEventListener('click', refreshProfile);
}
// Try to connect automatically
// Try to connect automatically if nostr extension is available
if (window.nostr) {
// If extension is available, try to connect with it
try {
const pubkey = await connectWithExtension();
if (pubkey) {
// We already set the auth state in connectWithExtension, but let's log success
console.log(`Successfully authenticated with pubkey: ${pubkey.substring(0, 8)}...`);
// Store the session expiry in localStorage for UI purposes
const secureStorage = new SecureStorageService();
const session = authManager.authService.getCurrentSession();
if (session) {
secureStorage.set('session_info', {
expiresAt: session.expiresAt,
createdAt: session.createdAt
});
console.log(`Session expires at ${new Date(session.expiresAt).toLocaleString()}`);
}
// Make double sure the auth manager has the correct state
authManager.updateAuthStateFromStorage();
console.log('Auth state after connection:', authManager.isAuthenticated() ? 'Authenticated' : 'Not authenticated');
} else {
console.warn('Failed to get pubkey from extension');
authManager.setAuthenticated(false);
}
} catch (error) {
console.error('Error auto-connecting with extension:', error);
// Clear authentication state on error
authManager.setAuthenticated(false);
// Show profile container even if connection fails
if (profileContainer) {
profileContainer.classList.remove('hidden');
@ -427,8 +385,6 @@ document.addEventListener('DOMContentLoaded', async () => {
} else {
// Even without an extension, show the profile container with default information
console.log('No Nostr extension available, showing default profile');
// Ensure authentication state is cleared
authManager.setAuthenticated(false);
if (profileContainer) {
profileContainer.classList.remove('hidden');
updateProfileWithDefaults();

@ -7,10 +7,10 @@ import * as nostrTools from 'nostr-tools';
// Import service classes
import { defaultServerConfig } from './config';
import { HttpService } from './services/HttpService';
import { NostrService } from './services/NostrService';
import { ReceivedEvent } from './services/NostrEventService';
import { decryptKeyWithNostrExtension, decryptKeyWithNostrTools, decryptWithWebCrypto } from './utils/crypto-utils';
import { NostrService } from './services/NostrService';
import { UiService } from './services/UiService';
import { decryptKeyWithNostrExtension, decryptKeyWithNostrTools, decryptWithWebCrypto } from './utils/crypto-utils';
// Module-level service instances
let uiService: UiService;

@ -2,10 +2,10 @@
* server-ui.ts
* Entry point for the 1120 server UI
*/
import './navbar-diagnostics'; // Import diagnostics first
import './navbar'; // Import navbar component
import './navbar-init'; // Import navbar initialization
import * as nostrTools from 'nostr-tools';
import { initServerUI } from './components/ServerUI';
import './debug-events'; // Import debug script
@ -191,7 +191,7 @@ function setupServerIdentityManager(): void {
// Toggle format button
toggleFormatBtn.addEventListener('click', () => {
if (!serverNsec || !serverPubkeyHex || !serverPubkeyNpub) return;
if (!serverNsec || !serverPubkeyHex || !serverPubkeyNpub) {return;}
isShowingNpub = !isShowingNpub;
@ -208,7 +208,7 @@ function setupServerIdentityManager(): void {
// Copy button
copyServerNpubBtn.addEventListener('click', () => {
if (!serverNsec) return;
if (!serverNsec) {return;}
navigator.clipboard.writeText(serverNpubInput.value)
.then(() => {

@ -1,30 +1,11 @@
/**
* AuthenticationService.ts
*
* Provides authentication services using Nostr protocol.
* Implements NIP-98 (HTTP Auth) for authenticated requests.
* Stateless authentication service that directly uses the Nostr extension.
* Removed all localStorage and session dependencies.
*/
import * as nostrTools from 'nostr-tools';
import { SecureStorageService } from './SecureStorageService';
/**
* Interface for auth session data
*/
export interface AuthSession {
/** User's public key in hex format */
pubkey: string;
/** User's public key in bech32/npub format */
npub: string;
/** Signature proving ownership */
signature: string;
/** When the session was created (timestamp) */
createdAt: number;
/** When the session expires (timestamp) */
expiresAt: number;
/** Optional refresh token for extending the session */
refreshToken?: string;
}
import type * as nostrTools from 'nostr-tools';
/**
* Interface for authentication operation results
@ -32,14 +13,15 @@ export interface AuthSession {
export interface AuthResult {
/** Whether the operation was successful */
success: boolean;
/** The session if successful */
session?: AuthSession;
/** User's public key if successful */
pubkey?: string;
/** Error message if unsuccessful */
error?: string;
}
/**
* Interface for NIP-98 HTTP Auth event
* Interface for NIP-98 HTTP Auth event (maintained for backwards compatibility)
* @deprecated This is maintained for backwards compatibility only
*/
export interface Nip98AuthEvent extends nostrTools.Event {
/** Event tags including method, uri, etc. */
@ -47,119 +29,78 @@ export interface Nip98AuthEvent extends nostrTools.Event {
}
/**
* HTTP method types for NIP-98
* HTTP method types for NIP-98 (maintained for backwards compatibility)
* @deprecated This is maintained for backwards compatibility only
*/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
/**
* Main authentication service class
* Event name for authentication state changes
*/
const AUTH_STATE_CHANGED_EVENT = 'auth-state-changed';
/**
* Main authentication service class - uses direct Nostr extension interaction
*/
export class AuthenticationService {
private secureStorage: SecureStorageService;
private currentSession: AuthSession | null = null;
// Cache for pubkey (in-memory only, not persisted)
private cachedPubkey: string | null = null;
// Storage keys
private readonly SESSION_KEY = 'authSession';
private readonly REFRESH_TOKEN_KEY = 'refreshToken';
// Default duration (24 hours) in milliseconds
private readonly SESSION_DURATION = 24 * 60 * 60 * 1000;
// Event name for authentication state changes
private readonly AUTH_STATE_CHANGED_EVENT = 'auth-state-changed';
// Listeners for auth state changes
private listeners: ((authenticated: boolean, pubkey?: string) => void)[] = [];
/**
* Constructor
*/
constructor() {
this.secureStorage = new SecureStorageService();
this.loadSessionFromStorage();
this.setupEventHandlers();
// Nothing to initialize - stateless design
}
/**
* Authenticate with Nostr extension and create a signed session
* Authenticate with Nostr extension
* This simply requests the pubkey from the extension
*
* @returns Promise resolving to authentication result
*/
public async authenticate(): Promise<AuthResult> {
try {
// Check if Nostr extension exists
// Check if a Nostr extension is available
if (!window.nostr) {
return {
success: false,
error: 'No Nostr extension detected. Please install a NIP-07 compatible extension.'
error: 'Nostr extension not found. Please install Alby or nos2x.'
};
}
// Get public key from extension
// Directly request the public key from the Nostr extension
const pubkey = await window.nostr.getPublicKey();
if (!pubkey) {
if (pubkey) {
// Cache the pubkey in memory
this.cachedPubkey = pubkey;
// Notify listeners of authentication change
this.notifyAuthStateChanged(true, pubkey);
// Set the global nostrPubkey for backwards compatibility with existing code
if (window) {
window.nostrPubkey = pubkey;
}
return {
success: false,
error: 'Failed to get public key from extension.'
};
}
// Generate a challenge the user must sign to prove key ownership
const challenge = `Authenticate for HTTP-over-Nostr at ${window.location.hostname} at ${Date.now()}`;
// Ask user to sign the challenge
let signature: string;
try {
// Use signEvent since signSchnorr may not be available in all extensions
const challengeEvent = {
kind: 27235, // NIP-98 HTTP Auth kind
created_at: Math.floor(Date.now() / 1000),
tags: [['challenge', challenge]],
content: '',
success: true,
pubkey
};
const signedEvent = await window.nostr.signEvent(challengeEvent);
signature = signedEvent.sig || '';
} catch (error) {
return {
success: false,
error: 'Failed to sign authentication challenge. User may have rejected the request.'
};
}
// Assume signature is valid if the extension signed it
// The extension handles the verification internally
const isValid = !!signature;
if (!isValid) {
return {
success: false,
error: 'Signature verification failed.'
};
}
// Create session
const now = Date.now();
const session: AuthSession = {
pubkey,
npub: nostrTools.nip19.npubEncode(pubkey),
signature,
createdAt: now,
expiresAt: now + this.SESSION_DURATION,
refreshToken: this.generateRefreshToken()
};
// Store session
this.currentSession = session;
this.persistSession(session);
// Notify listeners
this.notifyAuthStateChanged(true, pubkey);
return {
success: true,
session
success: false,
error: 'Failed to get public key'
};
} catch (error) {
console.error('Authentication error:', error);
// Return error result
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown authentication error'
@ -168,158 +109,103 @@ export class AuthenticationService {
}
/**
* Check if the user is currently authenticated with a valid session
*
* @returns True if authenticated with a valid session
* Check if the user is currently authenticated by attempting to fetch their pubkey
* @returns Promise resolving to boolean indicating authentication status
*/
public isAuthenticated(): boolean {
if (!this.currentSession) {
return false;
}
// Check if session is expired
if (this.currentSession.expiresAt < Date.now()) {
// Try to refresh the session if possible
if (this.currentSession.refreshToken) {
this.refreshSession().catch(error => {
console.error('Failed to refresh session:', error);
});
public async checkAuthentication(): Promise<boolean> {
try {
// If we already have a cached pubkey, verify it's still valid
if (this.cachedPubkey) {
return true;
}
// No cached pubkey, try to get it from the extension
if (window.nostr) {
try {
const pubkey = await window.nostr.getPublicKey();
if (pubkey) {
this.cachedPubkey = pubkey;
// Set the global nostrPubkey for backwards compatibility
if (window) {
window.nostrPubkey = pubkey;
}
return true;
}
} catch (e) {
// Error getting pubkey, user is not authenticated
console.log('Failed to get pubkey from extension:', e);
return false;
}
}
return false;
} catch {
return false;
}
return true;
}
/**
* Get the current session if authenticated
* Check if the user is currently authenticated (synchronous version)
* This uses the cached pubkey and doesn't make a new request
*
* @returns The current session or null if not authenticated
* @returns True if we have a cached pubkey
*/
public getCurrentSession(): AuthSession | null {
if (this.isAuthenticated()) {
return this.currentSession;
}
return null;
public isAuthenticated(): boolean {
return this.cachedPubkey !== null;
}
/**
* Get the current user's public key
* If not cached, will try to fetch it from the extension
*
* @returns The user's public key or null if not authenticated
* @returns Promise resolving to the user's public key or null
*/
public async getPubkey(): Promise<string | null> {
// If we have a cached pubkey, return it
if (this.cachedPubkey) {
return this.cachedPubkey;
}
// Try to get pubkey from extension
try {
if (window.nostr) {
const pubkey = await window.nostr.getPublicKey();
if (pubkey) {
this.cachedPubkey = pubkey;
return pubkey;
}
}
} catch (e) {
console.error('Error getting pubkey:', e);
}
return null;
}
/**
* Get the current user's public key (synchronous version)
* This only returns the cached pubkey and doesn't make a new request
*
* @returns The cached pubkey or null
*/
public getCurrentUserPubkey(): string | null {
return this.currentSession?.pubkey || null;
return this.cachedPubkey;
}
/**
* Logout the current user
* Clear the cached pubkey (logout)
*/
public logout(): void {
const wasPreviouslyAuthenticated = this.isAuthenticated();
const previousPubkey = this.getCurrentUserPubkey();
// Clear the cached pubkey
this.cachedPubkey = null;
this.currentSession = null;
this.secureStorage.remove(this.SESSION_KEY);
this.secureStorage.remove(this.REFRESH_TOKEN_KEY);
// Only notify if there was an actual state change
if (wasPreviouslyAuthenticated && previousPubkey) {
this.notifyAuthStateChanged(false, previousPubkey);
} else if (wasPreviouslyAuthenticated) {
this.notifyAuthStateChanged(false);
}
}
/**
* Create a signed NIP-98 HTTP Auth event
*
* @param method HTTP method
* @param url Request URL
* @param payload Optional request payload/body
* @returns Promise resolving to signed event or null if failed
*/
public async createAuthEvent(
method: HttpMethod,
url: string,
payload?: string
): Promise<Nip98AuthEvent | null> {
try {
// Ensure user is authenticated
if (!this.isAuthenticated() || !window.nostr) {
return null;
}
// Parse URL to get hostname and path
const parsedUrl = new URL(url);
// Create tags according to NIP-98
const tags: string[][] = [
['method', method],
['uri', url],
];
// Add content-type and digest for POST and PUT requests
if ((method === 'POST' || method === 'PUT') && payload) {
tags.push(['payload', payload]);
tags.push(['content-type', 'application/json']);
// Add SHA-256 digest of the payload
const digest = await this.sha256Digest(payload);
tags.push(['digest', `SHA-256=${digest}`]);
}
// Add created timestamp
const created = Math.floor(Date.now() / 1000);
tags.push(['created', created.toString()]);
// Create the event
const event = {
kind: 27235, // NIP-98 HTTP Auth
created_at: created,
tags,
content: '',
pubkey: this.currentSession!.pubkey
};
// Sign the event with NIP-07 extension
const signedEvent = await window.nostr.signEvent(event);
return signedEvent as Nip98AuthEvent;
} catch (error) {
console.error('Error creating NIP-98 auth event:', error);
return null;
}
}
/**
* Create HTTP headers for NIP-98 authentication
*
* @param event Signed NIP-98 auth event
* @returns Headers object with Authorization header
*/
public createAuthHeaders(event: Nip98AuthEvent): Headers {
const headers = new Headers();
const authValue = `Nostr ${JSON.stringify(event)}`;
headers.append('Authorization', authValue);
return headers;
}
/**
* Extend the current session's expiration
*
* @param extendBy Milliseconds to extend the session by (defaults to SESSION_DURATION)
* @returns True if session was extended
*/
public extendSession(extendBy?: number): boolean {
if (!this.currentSession) {
return false;
// Clear global reference
if (window) {
window.nostrPubkey = undefined;
}
const extension = extendBy || this.SESSION_DURATION;
this.currentSession.expiresAt = Date.now() + extension;
this.persistSession(this.currentSession);
return true;
// Notify listeners
this.notifyAuthStateChanged(false);
}
/**
@ -331,6 +217,10 @@ export class AuthenticationService {
public onAuthStateChanged(
callback: (authenticated: boolean, pubkey?: string) => void
): () => void {
// Add to internal listeners array
this.listeners.push(callback);
// Also listen for CustomEvent for backwards compatibility
const handler = (event: Event) => {
const authEvent = event as CustomEvent;
callback(
@ -339,94 +229,19 @@ export class AuthenticationService {
);
};
window.addEventListener(this.AUTH_STATE_CHANGED_EVENT, handler);
window.addEventListener(AUTH_STATE_CHANGED_EVENT, handler);
// Return function to remove the listener
// Return function to remove both listeners
return () => {
window.removeEventListener(this.AUTH_STATE_CHANGED_EVENT, handler);
};
}
/**
* Attempt to refresh the current session
*
* @returns Promise resolving to true if session was refreshed
*/
private async refreshSession(): Promise<boolean> {
// This would typically extend the session without requiring re-authentication
// For now, we'll just extend the current session
if (!this.currentSession || !this.currentSession.refreshToken) {
return false;
}
const storedRefreshToken = this.secureStorage.get<string>(this.REFRESH_TOKEN_KEY);
if (!storedRefreshToken || storedRefreshToken !== this.currentSession.refreshToken) {
return false;
}
// Extend the session
return this.extendSession();
}
/**
* Generate a random refresh token
*
* @returns A random string to use as refresh token
*/
private generateRefreshToken(): string {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
/**
* Persist session to secure storage
*
* @param session The session to persist
*/
private persistSession(session: AuthSession): void {
this.secureStorage.set(
this.SESSION_KEY,
session,
{ expiresIn: session.expiresAt - Date.now() }
);
if (session.refreshToken) {
this.secureStorage.set(
this.REFRESH_TOKEN_KEY,
session.refreshToken,
{ expiresIn: (session.expiresAt - Date.now()) + (7 * 24 * 60 * 60 * 1000) } // Refresh token valid for 1 week longer
);
}
}
/**
* Load session from secure storage
*/
private loadSessionFromStorage(): void {
const session = this.secureStorage.get<AuthSession>(this.SESSION_KEY);
if (session) {
this.currentSession = session;
// Remove from internal array
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
// Notify if a valid session is loaded
if (this.isAuthenticated()) {
this.notifyAuthStateChanged(true, session.pubkey);
}
}
}
/**
* Set up event handlers
*/
private setupEventHandlers(): void {
// Clean up expired items on page load
this.secureStorage.cleanExpired();
// Setup beforeunload event to save session
window.addEventListener('beforeunload', () => {
if (this.currentSession) {
this.persistSession(this.currentSession);
}
});
// Remove window event listener
window.removeEventListener(AUTH_STATE_CHANGED_EVENT, handler);
};
}
/**
@ -436,26 +251,50 @@ export class AuthenticationService {
* @param pubkey The user's pubkey (if authenticated)
*/
private notifyAuthStateChanged(authenticated: boolean, pubkey?: string): void {
// Notify internal listeners
for (const listener of this.listeners) {
try {
listener(authenticated, pubkey);
} catch (e) {
console.error('Error in auth state change listener:', e);
}
}
// Dispatch CustomEvent for backwards compatibility
window.dispatchEvent(
new CustomEvent(this.AUTH_STATE_CHANGED_EVENT, {
detail: { authenticated, pubkey }
new CustomEvent(AUTH_STATE_CHANGED_EVENT, {
detail: { authenticated, pubkey }
})
);
}
/**
* Calculate SHA-256
* Create a signed NIP-98 HTTP Auth event (stub)
*
* @param message Message to hash
* @returns Base64-encoded hash
* @deprecated This method is deprecated and maintained only for backwards compatibility
* @param method HTTP method
* @param url Request URL
* @param payload Optional request payload/body
* @returns Promise resolving to null since this functionality is deprecated
*/
private async sha256Digest(message: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const base64Hash = btoa(String.fromCharCode(...hashArray));
return base64Hash;
public async createAuthEvent(
method: HttpMethod,
url: string,
payload?: string
): Promise<Nip98AuthEvent | null> {
console.warn('AuthenticationService.createAuthEvent is deprecated and returns null');
return null;
}
/**
* Create HTTP headers for NIP-98 authentication (stub)
*
* @deprecated This method is deprecated and maintained only for backwards compatibility
* @param event NIP-98 auth event
* @returns Headers object with Authorization header
*/
public createAuthHeaders(event: Nip98AuthEvent): Headers {
console.warn('AuthenticationService.createAuthHeaders is deprecated');
return new Headers();
}
}

@ -7,6 +7,7 @@
*/
import type { NostrEvent } from '../relay';
import { EventChangeType } from './EventManager';
/**

@ -3,9 +3,10 @@
* Component for rendering detailed event information
*/
import { NostrEvent } from '../relay';
import { ReceivedEvent } from './NostrEventService';
import type { NostrEvent } from '../relay';
import { HttpFormatter } from './HttpFormatter';
import type { ReceivedEvent } from './NostrEventService';
/**
* Class for rendering event details in the UI
@ -84,7 +85,7 @@ export class EventDetailsRenderer {
isResponse: boolean,
eventTime: string
): void {
if (!this.eventDetails) return;
if (!this.eventDetails) {return;}
this.eventDetails.innerHTML = `
<div class="event-details-header">
@ -209,10 +210,10 @@ ${JSON.stringify(event, null, 2)}
* Set up tab button click handlers
*/
private setupTabButtons(): void {
if (!this.eventDetails) return;
if (!this.eventDetails) {return;}
const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
if (tabButtons.length === 0) return;
if (tabButtons.length === 0) {return;}
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
@ -284,19 +285,19 @@ ${JSON.stringify(event, null, 2)}
): Promise<void> {
try {
// Skip if the user has navigated to another event
if (this.currentEventId !== eventId) return;
if (this.currentEventId !== eventId) {return;}
// 1. Load related events first (fast operation)
await this.loadRelatedEvents(eventId, event, isRequest);
// Skip if the user has navigated to another event
if (this.currentEventId !== eventId) return;
if (this.currentEventId !== eventId) {return;}
// 2. Set up action buttons
this.setupActionButtons(eventId, event, isRequest);
// Skip if the user has navigated to another event
if (this.currentEventId !== eventId) return;
if (this.currentEventId !== eventId) {return;}
// 3. Get the HTTP content
const httpContent = receivedEvent.decrypted ?
@ -307,7 +308,7 @@ ${JSON.stringify(event, null, 2)}
this.updateRawContent(eventId, httpContent, receivedEvent.decrypted);
// Skip if the user has navigated to another event
if (this.currentEventId !== eventId) return;
if (this.currentEventId !== eventId) {return;}
// 5. Update formatted content (most expensive operation)
this.updateFormattedContent(eventId, httpContent, isRequest, isResponse || is21121Event, receivedEvent.decrypted);
@ -342,7 +343,7 @@ ${JSON.stringify(event, null, 2)}
isRequest: boolean
): Promise<void> {
const relatedEventsContainer = document.getElementById('related-events-container');
if (!relatedEventsContainer) return;
if (!relatedEventsContainer) {return;}
// Get related events
const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : [];
@ -395,7 +396,7 @@ ${JSON.stringify(event, null, 2)}
isRequest: boolean
): void {
const actionsContainer = document.getElementById(`http-actions-${eventId}`);
if (!actionsContainer) return;
if (!actionsContainer) {return;}
// Get related events count
const relatedIds = event.id ? (this.relatedEvents.get(event.id) || []) : [];
@ -541,11 +542,11 @@ ${JSON.stringify(event, null, 2)}
private displayResponse21121Json(event: NostrEvent): void {
// Get the JSON container
const jsonContainer = document.getElementById('response21121Json');
if (!jsonContainer) return;
if (!jsonContainer) {return;}
// Get the pre element within the container
const preElement = jsonContainer.querySelector('pre.json-content');
if (!preElement) return;
if (!preElement) {return;}
// Format the JSON prettily
const formattedJson = JSON.stringify(event, null, 2);

@ -5,7 +5,9 @@
*/
import { NostrEvent } from '../relay';
import { EventManager, EventChangeType, ManagedEvent } from './EventManager';
import type { EventManager} from './EventManager';
import { EventChangeType, ManagedEvent } from './EventManager';
import { HttpFormatter } from './HttpFormatter';
/**
@ -69,7 +71,7 @@ export class EventDetailsRenderer {
* Show empty state when no event is selected
*/
private showEmptyState(): void {
if (!this.eventDetails) return;
if (!this.eventDetails) {return;}
this.eventDetails.innerHTML = `
<div class="empty-state">
@ -82,7 +84,7 @@ export class EventDetailsRenderer {
* Render the details of the currently selected event
*/
private renderEventDetails(): void {
if (!this.eventDetails) return;
if (!this.eventDetails) {return;}
// Get the selected event from the EventManager
const managedEvent = this.eventManager.getSelectedEvent();
@ -99,7 +101,7 @@ export class EventDetailsRenderer {
const is21121Event = event.kind === 21121;
// Determine the content to display
let httpContent = managedEvent.decrypted ?
const httpContent = managedEvent.decrypted ?
managedEvent.decryptedContent || event.content :
event.content;
@ -212,7 +214,7 @@ export class EventDetailsRenderer {
* Set up tab buttons for switching between raw and formatted views
*/
private setupTabButtons(): void {
if (!this.eventDetails) return;
if (!this.eventDetails) {return;}
const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
tabButtons.forEach(button => {
@ -240,7 +242,7 @@ export class EventDetailsRenderer {
* Set up links to related events
*/
private setupRelatedEventLinks(): void {
if (!this.eventDetails) return;
if (!this.eventDetails) {return;}
const relatedLinks = this.eventDetails.querySelectorAll('.related-event-link');
relatedLinks.forEach(link => {

@ -4,7 +4,8 @@
*/
import * as nostrTools from 'nostr-tools';
import { NostrEvent } from '../relay';
import type { NostrEvent } from '../relay';
/**
* Class for rendering events in the UI list

@ -5,8 +5,11 @@
*/
import * as nostrTools from 'nostr-tools';
import { NostrEvent } from '../relay';
import { EventManager, EventChangeType, ManagedEvent } from './EventManager';
import type { NostrEvent } from '../relay';
import type { EventManager} from './EventManager';
import { EventChangeType, ManagedEvent } from './EventManager';
/**
* Class for rendering events in the UI list
@ -61,7 +64,7 @@ export class EventListRenderer {
* Render existing events from the EventManager
*/
private renderExistingEvents(): void {
if (!this.eventsList) return;
if (!this.eventsList) {return;}
// Clear any existing content
this.eventsList.innerHTML = '';
@ -170,7 +173,7 @@ export class EventListRenderer {
* @param eventId The ID of the event to update
*/
private updateEventItem(eventId: string): void {
if (!this.eventsList) return;
if (!this.eventsList) {return;}
// Find the existing event item
const existingItem = this.eventsList.querySelector(`.event-item[data-id="${eventId}"]`);
@ -188,7 +191,7 @@ export class EventListRenderer {
* @param eventId The ID of the event to remove
*/
private removeEventItem(eventId: string): void {
if (!this.eventsList) return;
if (!this.eventsList) {return;}
const eventItem = this.eventsList.querySelector(`.event-item[data-id="${eventId}"]`);
if (eventItem) {
@ -210,7 +213,7 @@ export class EventListRenderer {
* @param eventId The ID of the event to highlight
*/
private highlightSelectedEvent(eventId: string): void {
if (!this.eventsList) return;
if (!this.eventsList) {return;}
// Remove selected class from all items
const allItems = this.eventsList.querySelectorAll('.event-item');
@ -234,11 +237,11 @@ export class EventListRenderer {
private checkIfToServer(event: NostrEvent): boolean {
// Get server pubkey from EventManager
const serverPubkey = this.eventManager.getServerPubkey();
if (!serverPubkey) return false;
if (!serverPubkey) {return false;}
// Check for p tag to identify recipient
const pTag = event.tags.find(tag => tag[0] === 'p');
if (!pTag || pTag.length <= 1) return false;
if (!pTag || pTag.length <= 1) {return false;}
// Check if the p tag matches our server pubkey
return (pTag[1] === serverPubkey);
@ -252,7 +255,7 @@ export class EventListRenderer {
private getRecipientDisplay(event: NostrEvent): string {
// Find recipient if any
const pTag = event.tags.find(tag => tag[0] === 'p');
if (!pTag || pTag.length <= 1) return '';
if (!pTag || pTag.length <= 1) {return '';}
return `<div class="recipient">To: ${pTag[1].substring(0, 8)}...</div>`;
}

@ -4,12 +4,12 @@
* with UI components and event services.
*/
import { EventManager } from './EventManager';
import { EventListRenderer } from './EventListRenderer.updated';
import { EventDetailsRenderer } from './EventDetailsRenderer.updated';
import { NostrRelayService } from './NostrRelayService';
import { EventListRenderer } from './EventListRenderer.updated';
import { EventManager } from './EventManager';
import { NostrCacheService } from './NostrCacheService';
import { NostrEventService } from './NostrEventService.updated';
import { NostrRelayService } from './NostrRelayService';
import { RelayStatusManager } from './RelayStatusManager';
/**

@ -3,8 +3,9 @@
* Test cases and examples for the EventManager service
*/
import type { NostrEvent } from '../relay';
import { EventManager, EventKind, EventChangeType } from './EventManager';
import { NostrEvent } from '../relay';
/**
* Create a sample NostrEvent for testing

@ -3,7 +3,7 @@
* Centralizes event data management for 21120 and 21121 events
*/
import { NostrEvent } from '../relay';
import type { NostrEvent } from '../relay';
// Event types we're managing
export enum EventKind {

@ -3,7 +3,7 @@
* Service for making HTTP requests
*/
import { HttpService } from './HttpService';
import type { HttpService } from './HttpService';
import { ToastNotifier } from './ToastNotifier';
/**

@ -7,6 +7,7 @@
// Import crypto utilities
import { encryptWithWebCrypto, decryptWithWebCrypto } from '../utils/crypto-utils';
import { ToastNotifier } from './ToastNotifier';
// Interface definitions

@ -4,12 +4,14 @@
* Automatically processes incoming 21120 requests to generate 21121 responses
*/
import { NostrEvent } from '../relay';
import { NostrService } from './NostrService';
import { EventManager, EventChangeType, EventKind } from './EventManager';
import { HttpClient } from './HttpClient';
import { ToastNotifier } from './ToastNotifier';
import type { NostrEvent } from '../relay';
import type { EventManager} from './EventManager';
import { EventChangeType, EventKind } from './EventManager';
import type { HttpClient } from './HttpClient';
import { Nostr21121Service } from './Nostr21121Service';
import type { NostrService } from './NostrService';
import { ToastNotifier } from './ToastNotifier';
/**
* Class for handling NIP-21121 HTTP response events
@ -69,7 +71,7 @@ export class Nostr21121EventHandler {
*/
private async handleNewEvent(eventId: string): Promise<void> {
const managedEvent = this.eventManager.getEvent(eventId);
if (!managedEvent) return;
if (!managedEvent) {return;}
const event = managedEvent.event;

@ -3,11 +3,12 @@
* Handles integration between HTTP content and NIP-21121 events
*/
import { NostrEvent } from '../../src/relay';
import { Nostr21121Service } from './Nostr21121Service';
import { NostrEventService } from './NostrEventService';
import { ToastNotifier } from './ToastNotifier';
import type { NostrEvent } from '../../src/relay';
import { HttpFormatter } from './HttpFormatter';
import type { Nostr21121Service } from './Nostr21121Service';
import type { NostrEventService } from './NostrEventService';
import { ToastNotifier } from './ToastNotifier';
/**
* Helper class for NIP-21121 integration with HTTP content
@ -66,7 +67,7 @@ export class Nostr21121IntegrationHelper {
try {
// Extract the event ID from the header
const eventIdMatch = eventDetails.querySelector('h3')?.textContent?.match(/ID: (\w+)\.\.\./);
if (!eventIdMatch || !eventIdMatch[1]) return;
if (!eventIdMatch || !eventIdMatch[1]) {return;}
const eventId = eventIdMatch[1];
@ -81,7 +82,7 @@ export class Nostr21121IntegrationHelper {
// Get the HTTP content
const httpContent = eventDetails.querySelector('.http-content')?.textContent;
if (!httpContent) return;
if (!httpContent) {return;}
// Execute the HTTP request
try {
@ -126,7 +127,7 @@ export class Nostr21121IntegrationHelper {
*/
private displayHttpResponse(responseContent: string): void {
// Create or get the modal
let modal = document.getElementById('httpResponseModal');
const modal = document.getElementById('httpResponseModal');
if (!modal) {
console.error('HTTP response modal not found in DOM');
return;
@ -239,7 +240,7 @@ async function executeHttpRequestWithFetch(requestContent: string): Promise<stri
// Extract the host
let host = '';
let headers: Record<string, string> = {};
const headers: Record<string, string> = {};
let body = '';
let inHeaders = true;

@ -3,8 +3,9 @@
* Handler for creating and managing NIP-21121 HTTP response events
*/
import { NostrEvent } from '../relay';
import { NostrService } from './NostrService';
import type { NostrEvent } from '../relay';
import type { NostrService } from './NostrService';
import { ToastNotifier } from './ToastNotifier';
/**

@ -2,8 +2,9 @@
* NIP-21121 Service for HTTP response events
*/
import { NostrEvent } from '../relay';
import * as nostrTools from 'nostr-tools';
import type { NostrEvent } from '../relay';
import { encryptWithNostrTools, encryptWithWebCrypto } from '../utils/crypto-utils';
/**
@ -91,7 +92,7 @@ export class Nostr21121Service {
console.log(`Using pubkey: ${pubKey.substring(0, 8)}...`);
// Initialize tags array
let tags: string[][] = [];
const tags: string[][] = [];
// Always add reference to the request event
if (requestEvent.id) {

@ -7,9 +7,10 @@
import * as nostrTools from 'nostr-tools';
import type { NostrEvent } from '../relay';
import type { NostrCacheService } from './NostrCacheService';
import type { NostrFilter } from './NostrEventService';
import type { NostrRelayService } from './NostrRelayService';
import type { NostrCacheService } from './NostrCacheService';
/**
* Service for working with kind 31120 events (HTTP-over-Nostr server registrations)
@ -153,7 +154,7 @@ export class Nostr31120Service {
* @returns Server pubkey or null if not found
*/
public getServerPubkeyFromEvent(event: NostrEvent): string | null {
if (event.kind !== 31120) return null;
if (event.kind !== 31120) {return null;}
// Find the d tag which contains the server pubkey
const dTag = event.tags.find(tag => tag[0] === 'd');

@ -9,9 +9,11 @@ import * as nostrTools from 'nostr-tools';
// Project imports
import type { NostrEvent } from '../relay';
import type { EventManager} from './EventManager';
import { EventKind, EventChangeType } from './EventManager';
import type { NostrCacheService, ProfileData } from './NostrCacheService';
import type { NostrRelayService } from './NostrRelayService';
import { EventManager, EventKind, EventChangeType } from './EventManager';
// Interface for a Nostr subscription
export interface NostrSubscription {
@ -272,7 +274,7 @@ export class NostrEventService {
const events = this.cacheService.getCachedEvents(relayUrl);
if (events) {
const event = events.find(e => e.id === eventId);
if (event) return event;
if (event) {return event;}
}
}
@ -280,7 +282,7 @@ export class NostrEventService {
const events = this.cacheService.getCachedEvents('memory');
if (events) {
const event = events.find(e => e.id === eventId);
if (event) return event;
if (event) {return event;}
}
return null;

@ -3,20 +3,19 @@
* Main service that coordinates Nostr protocol functionality by integrating specialized services
*/
// Project imports
import * as authManager from '../auth-manager';
import type { NostrEvent } from '../relay';
// Import auth manager to gate network requests
import * as authManager from '../auth-manager';
// Import NIP-98 HTTP Auth types
import type { Nip98AuthEvent } from './AuthenticationService';
import { Nostr21121Service } from './Nostr21121Service';
import { Nostr31120Service } from './Nostr31120Service';
import type { ProfileData } from './NostrCacheService';
import { NostrCacheService } from './NostrCacheService';
import type { NostrFilter, NostrSubscription } from './NostrEventService';
import { NostrEventService } from './NostrEventService';
import { NostrRelayService } from './NostrRelayService';
import { Nostr31120Service } from './Nostr31120Service';
import { Nostr21121Service } from './Nostr21121Service';
/**
* Class for managing Nostr functionality

@ -149,7 +149,7 @@ export class SecureStorageService {
for (const fullKey of ourKeys) {
const storedValue = localStorage.getItem(fullKey);
if (!storedValue) continue;
if (!storedValue) {continue;}
try {
const storedItem = JSON.parse(storedValue);

@ -19,7 +19,7 @@ export class ToastNotifier {
* Initialize the toast container
*/
private static initialize(): void {
if (this.initialized) return;
if (this.initialized) {return;}
this.toastContainer = document.getElementById('toast-container');
@ -109,7 +109,7 @@ export class ToastNotifier {
public static show(message: string, type: NotificationType = 'info', duration = 5000): void {
this.initialize();
if (!this.toastContainer) return;
if (!this.toastContainer) {return;}
// Create toast element
const toast = document.createElement('div');

@ -3,17 +3,19 @@
* Handles UI-related operations and DOM manipulation
*/
import * as nostrTools from 'nostr-tools';
import { NostrEvent } from '../relay';
import { HttpService } from './HttpService';
import { HttpClient } from './HttpClient';
import { NostrService } from './NostrService';
import { ReceivedEvent } from './NostrEventService';
import { RelayStatusManager } from './RelayStatusManager';
import { EventListRenderer } from './EventListRenderer';
import type { NostrEvent } from '../relay';
import { EventDetailsRenderer } from './EventDetailsRenderer';
import { Nostr21121ResponseHandler } from './Nostr21121ResponseHandler';
import { ToastNotifier } from './ToastNotifier';
import { EventListRenderer } from './EventListRenderer';
import { HttpClient } from './HttpClient';
import { HttpFormatter } from './HttpFormatter';
import type { HttpService } from './HttpService';
import { Nostr21121ResponseHandler } from './Nostr21121ResponseHandler';
import type { ReceivedEvent } from './NostrEventService';
import type { NostrService } from './NostrService';
import { RelayStatusManager } from './RelayStatusManager';
import { ToastNotifier } from './ToastNotifier';
/**
* Class for managing UI operations
@ -109,7 +111,7 @@ export class UiService {
*/
public addReceivedEvent(receivedEvent: ReceivedEvent): void {
const event = receivedEvent.event;
if (!event || !event.id) return;
if (!event || !event.id) {return;}
// Store the event
this.receivedEvents.set(event.id, receivedEvent);
@ -131,7 +133,7 @@ export class UiService {
* @param event The event to check relations for
*/
private checkForRelatedEvents(event: NostrEvent): void {
if (!event.id) return;
if (!event.id) {return;}
// Check if this is a response with an e tag referencing a request
if (event.kind === 21121) {
@ -181,7 +183,7 @@ export class UiService {
// Set up event handlers for interactive elements
const eventDetailsElement = this.eventDetailsRenderer.getEventDetailsElement();
if (!eventDetailsElement) return;
if (!eventDetailsElement) {return;}
// Handle "Execute HTTP Request" button
const executeButtons = eventDetailsElement.querySelectorAll('.execute-http-request-btn');
@ -295,7 +297,7 @@ export class UiService {
const tabId = (button as HTMLElement).dataset.tab;
if (tabId) {
const tab = dialog.querySelector(`#${tabId}`);
if (tab) tab.classList.add('active');
if (tab) {tab.classList.add('active');}
}
});
});

@ -5,8 +5,11 @@
export interface WebSocketOptions {
timeout?: number;
// eslint-disable-next-line no-unused-vars
onOpen?: (ws: WebSocket) => void;
// eslint-disable-next-line no-unused-vars
onMessage?: (parsedData: unknown) => void;
// eslint-disable-next-line no-unused-vars
onError?: (evt: Event) => void;
onClose?: () => void;
}
@ -47,7 +50,9 @@ export class WebSocketManager {
// Set up event handlers
if (this.ws) {
this.ws.onopen = () => {
if (timeoutId) clearTimeout(timeoutId);
if (timeoutId) {
clearTimeout(timeoutId);
}
this.connected = true;
console.log(`WebSocketManager: Connection to ${url} established successfully`);
if (options.onOpen && this.ws) {
@ -84,7 +89,9 @@ export class WebSocketManager {
this.ws.onerror = (errorEvt) => {
console.error(`WebSocketManager: Error on connection to ${url}:`, errorEvt);
if (timeoutId) clearTimeout(timeoutId);
if (timeoutId) {
clearTimeout(timeoutId);
}
if (options.onError) {
options.onError(errorEvt);
}
@ -95,7 +102,9 @@ export class WebSocketManager {
this.ws.onclose = (evt) => {
console.log(`WebSocketManager: Connection to ${url} closed. Code: ${evt.code}, Reason: ${evt.reason || 'No reason provided'}, Clean: ${evt.wasClean}`);
if (timeoutId) clearTimeout(timeoutId);
if (timeoutId) {
clearTimeout(timeoutId);
}
this.connected = false;
// Log the closing information more thoroughly
@ -115,7 +124,9 @@ export class WebSocketManager {
}
} catch (error) {
console.error('Error creating WebSocket connection:', error);
if (timeoutId) clearTimeout(timeoutId);
if (timeoutId) {
clearTimeout(timeoutId);
}
this.close();
reject(error);
}
@ -127,30 +138,30 @@ export class WebSocketManager {
*/
public async testConnection(url: string, timeout = 5000): Promise<boolean> {
try {
const ws = new WebSocket(url);
const testWs = new WebSocket(url);
let connected = false;
await new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
if (!connected) {
ws.close();
testWs.close();
reject(new Error('Connection timeout'));
}
}, timeout);
ws.onopen = () => {
testWs.onopen = () => {
clearTimeout(timeoutId);
connected = true;
resolve();
};
ws.onerror = (err) => {
testWs.onerror = (err) => {
clearTimeout(timeoutId);
reject(new Error(`WebSocket error: ${err.toString()}`));
};
});
ws.close();
testWs.close();
return true;
} catch {
return false;

@ -43,6 +43,16 @@ declare global {
// Add global authentication state tracking
authInProgress?: boolean;
currentSignedEvent?: NostrEvent;
// Add timestamp for last authentication request (for debouncing)
lastAuthRequestTime?: number;
// Add declaration for NostrLogin global object
nostrLogin?: any;
// Add declaration for nostrPubkey global property
nostrPubkey?: string;
// Add login status elements
loginStatus?: HTMLElement;
loginButton?: HTMLElement;
logoutButton?: HTMLElement;
}
}

@ -1098,12 +1098,39 @@ footer {
/* Login container */
.login-container {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
margin-right: 20px;
}
.login-status {
margin-top: 5px;
font-size: 14px;
#login-status {
font-size: 0.9rem;
padding: 4px 8px;
border-radius: 4px;
background-color: var(--background-secondary);
}
.auth-button {
padding: 4px 10px;
border-radius: 4px;
border: 1px solid var(--border-color);
background-color: var(--button-primary);
color: white;
cursor: pointer;
font-size: 0.9rem;
}
.auth-button:hover {
background-color: var(--button-primary-hover);
}
#logout-button {
background-color: var(--button-secondary);
}
#logout-button:hover {
background-color: var(--button-secondary-hover);
}
/* Hidden elements */
@ -2967,4 +2994,34 @@ footer {
.auth-required-message h3 {
color: var(--accent-color);
margin-top: 0;
}
/* Authentication container and button styles */
.auth-container {
display: flex;
align-items: center;
margin-left: auto;
margin-right: 10px;
}
.auth-btn {
font-size: 0.8em;
padding: 3px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.sign-in-btn {
background-color: var(--button-primary, #4285f4);
color: white;
border: none;
font-weight: 500;
padding: 5px 10px;
}
.sign-in-btn:hover {
background-color: var(--button-hover, #3367d6);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}