- 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.
210 lines
No EOL
5.6 KiB
TypeScript
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
|
|
}
|
|
}
|
|
|
|
} |