You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

216 lines
6.5 KiB

/**
* Session manager for active user sessions
*/
import type { NostrEvent } from '../../types/nostr.js';
export type AuthMethod = 'nip07' | 'nsec' | 'bunker' | 'anonymous';
export interface UserSession {
pubkey: string;
method: AuthMethod;
signer: (event: Omit<NostrEvent, 'sig' | 'id'>) => Promise<NostrEvent>;
createdAt: number;
}
// Simple store implementation for Svelte reactivity
function createStore<T>(initial: T) {
let value = initial;
const subscribers = new Set<(value: T) => void>();
return {
get value() {
return value;
},
set(newValue: T) {
value = newValue;
subscribers.forEach((fn) => fn(value));
},
subscribe(fn: (value: T) => void) {
subscribers.add(fn);
fn(value);
return () => subscribers.delete(fn);
}
};
}
class SessionManager {
private currentSession: UserSession | null = null;
public session = createStore<UserSession | null>(null);
/**
* Set current session
*/
setSession(session: UserSession, metadata?: Record<string, any>): void {
this.currentSession = session;
this.session.set(session);
// Store in localStorage for persistence
if (typeof window !== 'undefined') {
const sessionData: any = {
pubkey: session.pubkey,
method: session.method,
createdAt: session.createdAt
};
// Store method-specific metadata for restoration
if (metadata) {
sessionData.metadata = metadata;
}
localStorage.setItem('aitherboard_session', JSON.stringify(sessionData));
}
}
/**
* Get current session
*/
getSession(): UserSession | null {
return this.currentSession;
}
/**
* Check if user is logged in
*/
isLoggedIn(): boolean {
return this.currentSession !== null;
}
/**
* Get current pubkey
*/
getCurrentPubkey(): string | null {
return this.currentSession?.pubkey || null;
}
/**
* Sign event with current session
*/
async signEvent(event: Omit<NostrEvent, 'sig' | 'id'>): Promise<NostrEvent> {
if (!this.currentSession) {
throw new Error('No active session');
}
return this.currentSession.signer(event);
}
/**
* Clear session
*/
clearSession(): void {
this.currentSession = null;
this.session.set(null);
if (typeof window !== 'undefined') {
localStorage.removeItem('aitherboard_session');
}
}
/**
* Restore session from localStorage
* This will attempt to restore the session based on the auth method
*/
async restoreSession(): Promise<boolean> {
if (typeof window === 'undefined') return false;
const stored = localStorage.getItem('aitherboard_session');
if (!stored) return false;
try {
const data = JSON.parse(stored);
const { pubkey, method, metadata } = data;
if (!pubkey || !method) return false;
// Import auth handlers dynamically to avoid circular dependencies
switch (method) {
case 'nip07': {
// For NIP-07, we can restore by checking if extension is still available
const { isNIP07Available, getPublicKeyWithNIP07, signEventWithNIP07 } = await import('./nip07-signer.js');
if (isNIP07Available()) {
try {
// Verify the extension still has the same pubkey
const currentPubkey = await getPublicKeyWithNIP07();
if (currentPubkey === pubkey) {
this.setSession({
pubkey,
method: 'nip07',
signer: signEventWithNIP07,
createdAt: data.createdAt || Date.now()
});
return true;
}
} catch {
// Extension error, can't restore
return false;
}
}
return false;
}
case 'bunker': {
// For bunker, restore if we have the bunker URI
if (metadata?.bunkerUri) {
const { connectBunker, signEventWithBunker } = await import('./bunker-signer.js');
try {
const connection = await connectBunker(metadata.bunkerUri);
if (connection.pubkey === pubkey) {
this.setSession({
pubkey,
method: 'bunker',
signer: async (event) => signEventWithBunker(event, connection),
createdAt: data.createdAt || Date.now()
}, { bunkerUri: metadata.bunkerUri });
return true;
}
} catch {
// Bunker connection failed
return false;
}
}
return false;
}
case 'anonymous': {
// For anonymous, we can restore if the encrypted key is stored
// The key is stored in IndexedDB, we just need to verify it exists
const { getStoredAnonymousKey } = await import('./anonymous-signer.js');
// We can't restore without password, but we can check if key exists
// For now, we'll just restore the pubkey and let signer fail if password is wrong
// In practice, user would need to re-enter password
if (pubkey) {
// Check if key exists in storage
try {
// This will fail without password, but we can still restore session
// The signer will need password on first use
const { signEventWithAnonymous } = await import('./anonymous-signer.js');
this.setSession({
pubkey,
method: 'anonymous',
signer: async (event) => {
// This will fail without password - user needs to re-authenticate
throw new Error('Anonymous session requires password. Please log in again.');
},
createdAt: data.createdAt || Date.now()
});
// Note: This session won't work until user re-authenticates
return true;
} catch {
return false;
}
}
return false;
}
case 'nsec': {
// nsec can't be restored without password (security)
// Clear the stored session
localStorage.removeItem('aitherboard_session');
return false;
}
default:
return false;
}
} catch (error) {
console.error('Error restoring session:', error);
// Clear corrupted session data
localStorage.removeItem('aitherboard_session');
return false;
}
}
}
export const sessionManager = new SessionManager();