231 lines
6.2 KiB
TypeScript

import { nostrService } from './nostr'
import { keyManagementService } from './keyManagement'
import type { NostrConnectState, NostrProfile } from '@/types/nostr'
/**
* Nostr authentication service using local key management
* Keys are stored encrypted in IndexedDB and decrypted using recovery phrase
*/
export class NostrAuthService {
private state: NostrConnectState = {
connected: false,
pubkey: null,
profile: null,
}
private listeners: Set<(state: NostrConnectState) => void> = new Set()
private unlockedPrivateKey: string | null = null
constructor() {
if (typeof window !== 'undefined') {
void this.loadStateFromStorage()
this.setupMessageListener()
}
}
subscribe(callback: (state: NostrConnectState) => void): () => void {
this.listeners.add(callback)
callback(this.state)
return () => {
this.listeners.delete(callback)
}
}
getState(): NostrConnectState {
return { ...this.state }
}
/**
* Check if account exists
*/
async accountExists(): Promise<boolean> {
return keyManagementService.accountExists()
}
/**
* Create a new account (generate or import key)
* Returns recovery phrase and npub
*/
async createAccount(privateKey?: string): Promise<{
recoveryPhrase: string[]
npub: string
publicKey: string
}> {
const result = await keyManagementService.createAccount(privateKey)
// Set public key immediately
this.state = {
connected: false,
pubkey: result.publicKey,
profile: null,
}
nostrService.setPublicKey(result.publicKey)
void this.saveStateToStorage()
this.notifyListeners()
return result
}
/**
* Unlock account using recovery phrase
*/
async unlockAccount(recoveryPhrase: string[]): Promise<void> {
try {
const keys = await keyManagementService.unlockAccount(recoveryPhrase)
this.unlockedPrivateKey = keys.privateKey
this.state = {
connected: true,
pubkey: keys.publicKey,
profile: null,
}
nostrService.setPublicKey(keys.publicKey)
nostrService.setPrivateKey(keys.privateKey)
void this.saveStateToStorage()
this.notifyListeners()
void this.loadProfile()
} catch (e) {
console.error('Error unlocking account:', e)
throw new Error(`Failed to unlock account: ${e instanceof Error ? e.message : 'Unknown error'}`)
}
}
/**
* Connect using existing stored keys (if already unlocked)
* This is called when the app loads and keys are already available
*/
async connect(): Promise<void> {
// Check if account exists
const exists = await keyManagementService.accountExists()
if (!exists) {
throw new Error('No account found. Please create an account first.')
}
// Try to get public keys
const publicKeys = await keyManagementService.getPublicKeys()
if (!publicKeys) {
throw new Error('Account exists but public keys not found')
}
// Set public key but don't unlock private key yet
// Private key will be unlocked when user provides recovery phrase
this.state = {
connected: false,
pubkey: publicKeys.publicKey,
profile: null,
}
nostrService.setPublicKey(publicKeys.publicKey)
void this.saveStateToStorage()
this.notifyListeners()
void this.loadProfile()
}
/**
* Get the private key if unlocked
*/
getPrivateKey(): string | null {
return this.unlockedPrivateKey
}
/**
* Check if private key is unlocked
*/
isUnlocked(): boolean {
return this.unlockedPrivateKey !== null
}
disconnect(): void {
this.unlockedPrivateKey = null
this.state = {
connected: false,
pubkey: null,
profile: null,
}
// Clear keys from nostrService - the service stores keys internally, we just clear our reference
// The service will continue to work but won't have access to the keys
nostrService.setPrivateKey('')
nostrService.setPublicKey('')
void this.saveStateToStorage()
this.notifyListeners()
}
/**
* Delete account (remove all stored keys)
*/
async deleteAccount(): Promise<void> {
await keyManagementService.deleteAccount()
this.disconnect()
}
private async loadProfile(): Promise<void> {
if (!this.state.pubkey) {
return
}
try {
const profile = await nostrService.getProfile(this.state.pubkey)
if (profile) {
this.state.profile = profile
void this.saveStateToStorage()
this.notifyListeners()
}
} catch (e) {
console.error('Error loading profile:', e)
}
}
private setupMessageListener(): void {
window.addEventListener('storage', (e) => {
if (e.key === 'nostr_auth_state') {
this.loadStateFromStorage()
}
})
}
private async loadStateFromStorage(): Promise<void> {
try {
const { storageService } = await import('./storage/indexedDB')
const stored = await storageService.get<{
connected: boolean
pubkey: string | null
profile: NostrProfile | null
}>('nostr_auth_state', 'nostr_auth_storage')
if (stored) {
this.state = {
connected: stored.connected ?? false,
pubkey: stored.pubkey ?? null,
profile: stored.profile ?? null,
}
if (this.state.pubkey) {
nostrService.setPublicKey(this.state.pubkey)
}
// Note: private key is not stored, it must be unlocked with recovery phrase
}
} catch (e) {
console.error('Error loading state from storage:', e)
}
}
private async saveStateToStorage(): Promise<void> {
try {
// Only save public information, never private keys
const stateToSave = {
connected: this.state.connected,
pubkey: this.state.pubkey,
profile: this.state.profile,
}
const { storageService } = await import('./storage/indexedDB')
await storageService.set('nostr_auth_state', stateToSave, 'nostr_auth_storage')
} catch (e) {
console.error('Error saving state to storage:', e)
}
}
private notifyListeners(): void {
this.listeners.forEach((callback) => callback({ ...this.state }))
}
}
export const nostrAuthService = new NostrAuthService()