web-app/src/lib/nostr/identity.ts
padreug c05f40f1ec feat: Implement push notification system for admin announcements
- Add a notification manager to handle push notifications and integrate with Nostr events.
- Create a push notification service to manage subscription and permission requests.
- Introduce components for notification settings and permission prompts in the UI.
- Update Nostr store to manage push notification state and enable/disable functionality.
- Enhance NostrFeed to send notifications for new admin announcements.
- Implement test notification functionality for development purposes.
2025-07-12 18:10:33 +02:00

210 lines
No EOL
5.6 KiB
TypeScript

import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools'
import { SecureStorage } from '@/lib/crypto/encryption'
import { bytesToHex, hexToBytes } from '@/lib/utils/crypto'
export interface NostrIdentity {
privateKey: string
publicKey: string
npub: string
nsec: string
}
export interface NostrProfile {
name?: string
display_name?: string
about?: string
picture?: string
banner?: string
website?: string
nip05?: string
lud16?: string // Lightning address
}
export class IdentityManager {
private static readonly STORAGE_KEY = 'nostr_identity'
private static readonly PROFILE_KEY = 'nostr_profile'
/**
* Generate a new Nostr identity
*/
static generateIdentity(): NostrIdentity {
const privateKey = generateSecretKey()
const publicKey = getPublicKey(privateKey)
return {
privateKey: bytesToHex(privateKey),
publicKey,
npub: nip19.npubEncode(publicKey),
nsec: nip19.nsecEncode(privateKey)
}
}
/**
* Import identity from private key (hex or nsec format)
*/
static importIdentity(privateKeyInput: string): NostrIdentity {
let privateKeyBytes: Uint8Array
try {
// Try to decode as nsec first
if (privateKeyInput.startsWith('nsec')) {
const decoded = nip19.decode(privateKeyInput)
if (decoded.type === 'nsec') {
privateKeyBytes = decoded.data
} else {
throw new Error('Invalid nsec format')
}
} else {
// Try as hex string
if (privateKeyInput.length !== 64) {
throw new Error('Private key must be 64 hex characters')
}
privateKeyBytes = hexToBytes(privateKeyInput)
}
if (privateKeyBytes.length !== 32) {
throw new Error('Private key must be 32 bytes')
}
const publicKey = getPublicKey(privateKeyBytes)
return {
privateKey: bytesToHex(privateKeyBytes),
publicKey,
npub: nip19.npubEncode(publicKey),
nsec: nip19.nsecEncode(privateKeyBytes)
}
} catch (error) {
throw new Error(`Failed to import identity: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Save identity to localStorage (encrypted)
*/
static async saveIdentity(identity: NostrIdentity, password?: string): Promise<void> {
try {
let dataToStore: any = identity
if (password) {
if (!SecureStorage.isSupported()) {
throw new Error('Secure encryption is not supported in this browser')
}
// Encrypt sensitive data using Web Crypto API
const sensitiveData = JSON.stringify({
privateKey: identity.privateKey,
nsec: identity.nsec
})
const encryptedData = await SecureStorage.encrypt(sensitiveData, password)
dataToStore = {
publicKey: identity.publicKey,
npub: identity.npub,
encrypted: true,
encryptedData
}
}
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(dataToStore))
} catch (error) {
throw new Error(`Failed to save identity: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Load identity from localStorage
*/
static async loadIdentity(password?: string): Promise<NostrIdentity | null> {
try {
const stored = localStorage.getItem(this.STORAGE_KEY)
if (!stored) return null
const storedData = JSON.parse(stored)
if (storedData.encrypted && password) {
if (!SecureStorage.isSupported()) {
throw new Error('Secure encryption is not supported in this browser')
}
// Decrypt sensitive data
const decryptedSensitiveData = await SecureStorage.decrypt(storedData.encryptedData, password)
const { privateKey, nsec } = JSON.parse(decryptedSensitiveData)
return {
privateKey,
publicKey: storedData.publicKey,
npub: storedData.npub,
nsec
}
} else if (storedData.encrypted && !password) {
throw new Error('Password required to decrypt stored identity')
}
// Non-encrypted identity (legacy support)
const identity = storedData as NostrIdentity
// Validate the loaded identity
if (!identity.privateKey || !identity.publicKey) {
throw new Error('Invalid stored identity')
}
return identity
} catch (error) {
console.error('Failed to load identity:', error)
throw error
}
}
/**
* Clear stored identity
*/
static clearIdentity(): void {
localStorage.removeItem(this.STORAGE_KEY)
localStorage.removeItem(this.PROFILE_KEY)
}
/**
* Check if identity exists in storage
*/
static hasStoredIdentity(): boolean {
return !!localStorage.getItem(this.STORAGE_KEY)
}
/**
* Check if stored identity is encrypted
*/
static isStoredIdentityEncrypted(): boolean {
try {
const stored = localStorage.getItem(this.STORAGE_KEY)
if (!stored) return false
const storedData = JSON.parse(stored)
return !!storedData.encrypted
} catch {
return false
}
}
/**
* Save user profile
*/
static saveProfile(profile: NostrProfile): void {
localStorage.setItem(this.PROFILE_KEY, JSON.stringify(profile))
}
/**
* Load user profile
*/
static loadProfile(): NostrProfile | null {
try {
const stored = localStorage.getItem(this.PROFILE_KEY)
return stored ? JSON.parse(stored) : null
} catch (error) {
console.error('Failed to load profile:', error)
return null
}
}
}