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
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();
|
|
|