226 lines
5.9 KiB
TypeScript
226 lines
5.9 KiB
TypeScript
import { nostrService } from './nostr'
|
|
import { keyManagementService } from './keyManagement'
|
|
import type { NostrConnectState } 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') {
|
|
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)
|
|
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)
|
|
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)
|
|
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('')
|
|
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
|
|
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 loadStateFromStorage(): void {
|
|
try {
|
|
const stored = localStorage.getItem('nostr_auth_state')
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored)
|
|
this.state = {
|
|
connected: parsed.connected ?? false,
|
|
pubkey: parsed.pubkey ?? null,
|
|
profile: parsed.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 saveStateToStorage(): void {
|
|
try {
|
|
// Only save public information, never private keys
|
|
const stateToSave = {
|
|
connected: this.state.connected,
|
|
pubkey: this.state.pubkey,
|
|
profile: this.state.profile,
|
|
}
|
|
localStorage.setItem('nostr_auth_state', JSON.stringify(stateToSave))
|
|
} 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()
|