1- import { NostrService , NostrEvent } from '../types/nostr' ;
2- import { Settings } from '../types/settings' ;
1+ import { nip44 } from 'nostr-crypto-utils' ;
2+ import { hexToBytes } from 'nostr-crypto-utils' ;
3+ import type { NostrService , NostrEvent , EncryptionVersion } from '../types/nostr' ;
4+ import type { Settings } from '../types/settings' ;
5+
6+ /**
7+ * Options for SettingsManager construction.
8+ */
9+ export interface SettingsManagerOptions {
10+ /** The NostrService implementation for sending/querying DMs */
11+ nostrService : NostrService ;
12+ /** The user's hex-encoded public key */
13+ userPubkey : string ;
14+ /**
15+ * The user's hex-encoded private key.
16+ * Required when encryptionVersion is 'nip44' for deriving conversation keys.
17+ * When using 'nip04', the consuming application handles encryption in sendDirectMessage.
18+ */
19+ privateKey ?: string ;
20+ /**
21+ * Encryption version to use for settings DMs.
22+ * - 'nip04': Legacy NIP-04 encryption (default). Encryption is handled by the
23+ * consuming application's NostrService.sendDirectMessage implementation.
24+ * - 'nip44': Modern NIP-44 encryption. SettingsManager encrypts/decrypts content
25+ * directly using the provided privateKey before passing to NostrService.
26+ */
27+ encryptionVersion ?: EncryptionVersion ;
28+ }
329
430export class SettingsManager {
531 private nostrService : NostrService ;
632 private userPubkey : string ;
33+ private privateKey : string | undefined ;
34+ private encryptionVersion : EncryptionVersion ;
735 private cachedSettings : Settings | null = null ;
836
9- constructor ( nostrService : NostrService , userPubkey : string ) {
10- this . nostrService = nostrService ;
11- this . userPubkey = userPubkey ;
37+ constructor ( options : SettingsManagerOptions ) ;
38+ /**
39+ * @deprecated Use the options object constructor instead.
40+ * Retained for backward compatibility.
41+ */
42+ constructor ( nostrService : NostrService , userPubkey : string ) ;
43+ constructor (
44+ optionsOrService : SettingsManagerOptions | NostrService ,
45+ userPubkey ?: string
46+ ) {
47+ if ( userPubkey !== undefined ) {
48+ // Legacy two-argument constructor
49+ this . nostrService = optionsOrService as NostrService ;
50+ this . userPubkey = userPubkey ;
51+ this . encryptionVersion = 'nip04' ;
52+ } else {
53+ const opts = optionsOrService as SettingsManagerOptions ;
54+ this . nostrService = opts . nostrService ;
55+ this . userPubkey = opts . userPubkey ;
56+ this . privateKey = opts . privateKey ;
57+ this . encryptionVersion = opts . encryptionVersion ?? 'nip04' ;
58+
59+ if ( this . encryptionVersion === 'nip44' && ! this . privateKey ) {
60+ throw new Error (
61+ 'privateKey is required when using NIP-44 encryption'
62+ ) ;
63+ }
64+ }
1265 }
1366
1467 /**
@@ -24,6 +77,51 @@ export class SettingsManager {
2477 } ;
2578 }
2679
80+ /**
81+ * Encrypt content using NIP-44.
82+ * @param plaintext - The plaintext content to encrypt
83+ * @returns Base64-encoded NIP-44 encrypted payload
84+ */
85+ private encryptNip44 ( plaintext : string ) : string {
86+ if ( ! this . privateKey ) {
87+ throw new Error ( 'privateKey is required for NIP-44 encryption' ) ;
88+ }
89+ const privkeyBytes = hexToBytes ( this . privateKey ) ;
90+ const conversationKey = nip44 . getConversationKey ( privkeyBytes , this . userPubkey ) ;
91+ return nip44 . encrypt ( plaintext , conversationKey ) ;
92+ }
93+
94+ /**
95+ * Decrypt content using NIP-44.
96+ * @param payload - Base64-encoded NIP-44 encrypted payload
97+ * @returns Decrypted plaintext string
98+ */
99+ private decryptNip44 ( payload : string ) : string {
100+ if ( ! this . privateKey ) {
101+ throw new Error ( 'privateKey is required for NIP-44 decryption' ) ;
102+ }
103+ const privkeyBytes = hexToBytes ( this . privateKey ) ;
104+ const conversationKey = nip44 . getConversationKey ( privkeyBytes , this . userPubkey ) ;
105+ return nip44 . decrypt ( payload , conversationKey ) ;
106+ }
107+
108+ /**
109+ * Attempt to decrypt event content, trying NIP-44 first if configured,
110+ * then falling back to treating content as plaintext JSON (for NIP-04,
111+ * where the NostrService handles decryption before returning events).
112+ */
113+ private decryptContent ( event : NostrEvent ) : string {
114+ if ( this . encryptionVersion === 'nip44' && this . privateKey ) {
115+ try {
116+ return this . decryptNip44 ( event . content ) ;
117+ } catch {
118+ // Fall through to try as plain JSON (may be a legacy NIP-04 event
119+ // already decrypted by the NostrService)
120+ }
121+ }
122+ return event . content ;
123+ }
124+
27125 /**
28126 * Get settings from Nostr DMs
29127 */
@@ -35,11 +133,12 @@ export class SettingsManager {
35133 kinds : [ 4 ] , // kind 4 is for encrypted direct messages
36134 limit : 100
37135 } ) ;
38-
136+
39137 // Filter for settings DMs
40138 const settingsDMs = events . filter ( ( event : NostrEvent ) => {
41139 try {
42- const content = JSON . parse ( event . content ) ;
140+ const decrypted = this . decryptContent ( event ) ;
141+ const content = JSON . parse ( decrypted ) ;
43142 return content . type === 'settings' ;
44143 } catch {
45144 return false ;
@@ -51,7 +150,8 @@ export class SettingsManager {
51150 }
52151
53152 // Get latest settings
54- const latestSettings = JSON . parse ( settingsDMs [ 0 ] . content ) ;
153+ const decrypted = this . decryptContent ( settingsDMs [ 0 ] ) ;
154+ const latestSettings = JSON . parse ( decrypted ) ;
55155 return latestSettings . data ;
56156 } catch ( error ) {
57157 console . error ( 'Error getting settings from DMs:' , error ) ;
@@ -78,7 +178,10 @@ export class SettingsManager {
78178 }
79179
80180 /**
81- * Save settings to DM
181+ * Save settings to DM.
182+ * When using NIP-44, content is encrypted before passing to NostrService.
183+ * When using NIP-04, content is passed as plaintext JSON and the
184+ * NostrService.sendDirectMessage implementation handles encryption.
82185 */
83186 async saveSettings ( settings : Settings ) : Promise < void > {
84187 const message = {
@@ -87,7 +190,13 @@ export class SettingsManager {
87190 timestamp : Date . now ( )
88191 } ;
89192
90- await this . nostrService . sendDirectMessage ( this . userPubkey , JSON . stringify ( message ) ) ;
193+ let content = JSON . stringify ( message ) ;
194+
195+ if ( this . encryptionVersion === 'nip44' ) {
196+ content = this . encryptNip44 ( content ) ;
197+ }
198+
199+ await this . nostrService . sendDirectMessage ( this . userPubkey , content ) ;
91200 this . cachedSettings = settings ;
92201 }
93202
@@ -100,4 +209,11 @@ export class SettingsManager {
100209 await this . saveSettings ( updated ) ;
101210 return updated ;
102211 }
212+
213+ /**
214+ * Get the current encryption version
215+ */
216+ getEncryptionVersion ( ) : EncryptionVersion {
217+ return this . encryptionVersion ;
218+ }
103219}
0 commit comments