identity.ts raw
1 import { AggregateRoot } from '../events/domain-event';
2 import { IdentityCreated, IdentityRenamed, IdentitySigned } from '../events/identity-events';
3 import {
4 IdentityId,
5 Nickname,
6 NostrKeyPair,
7 } from '../value-objects';
8 import type { IdentitySnapshot } from '../repositories/identity-repository';
9
10 /**
11 * Represents an unsigned Nostr event template.
12 * This is what gets passed to the sign method.
13 */
14 export interface UnsignedEvent {
15 kind: number;
16 created_at: number;
17 tags: string[][];
18 content: string;
19 }
20
21 /**
22 * Represents a signed Nostr event.
23 */
24 export interface SignedEvent extends UnsignedEvent {
25 id: string;
26 pubkey: string;
27 sig: string;
28 }
29
30 /**
31 * Signing function type - injected to avoid coupling to nostr-tools.
32 */
33 export type SigningFunction = (event: UnsignedEvent, privateKeyBytes: Uint8Array) => SignedEvent;
34
35 /**
36 * Encryption function types for NIP-04 and NIP-44.
37 */
38 export type EncryptFunction = (
39 privateKeyBytes: Uint8Array,
40 peerPubkey: string,
41 plaintext: string
42 ) => Promise<string>;
43
44 export type DecryptFunction = (
45 privateKeyBytes: Uint8Array,
46 peerPubkey: string,
47 ciphertext: string
48 ) => Promise<string>;
49
50 /**
51 * Identity entity - represents a Nostr identity with its keypair.
52 *
53 * This is an aggregate root that encapsulates all operations
54 * related to a single Nostr identity.
55 */
56 export class Identity extends AggregateRoot {
57 private readonly _id: IdentityId;
58 private _nickname: Nickname;
59 private readonly _keyPair: NostrKeyPair;
60 private readonly _createdAt: Date;
61
62 private constructor(
63 id: IdentityId,
64 nickname: Nickname,
65 keyPair: NostrKeyPair,
66 createdAt: Date
67 ) {
68 super();
69 this._id = id;
70 this._nickname = nickname;
71 this._keyPair = keyPair;
72 this._createdAt = createdAt;
73 }
74
75 // ─────────────────────────────────────────────────────────────────────────
76 // Factory Methods
77 // ─────────────────────────────────────────────────────────────────────────
78
79 /**
80 * Create a new identity with an optional private key.
81 * If no private key is provided, a new one will be generated.
82 *
83 * @param nickname - User-friendly name for this identity
84 * @param privateKey - Optional private key (hex or nsec format)
85 * @throws InvalidNicknameError if nickname is invalid
86 * @throws InvalidNostrKeyError if private key is invalid
87 */
88 static create(nickname: string, privateKey?: string): Identity {
89 const keyPair = privateKey
90 ? NostrKeyPair.fromPrivateKey(privateKey)
91 : NostrKeyPair.generate();
92
93 const identity = new Identity(
94 IdentityId.generate(),
95 Nickname.create(nickname),
96 keyPair,
97 new Date()
98 );
99
100 identity.addDomainEvent(
101 new IdentityCreated(
102 identity._id.value,
103 identity.publicKey,
104 identity.nickname
105 )
106 );
107
108 return identity;
109 }
110
111 /**
112 * Reconstitute an identity from storage.
113 * This bypasses validation since data comes from trusted storage.
114 */
115 static fromSnapshot(snapshot: IdentitySnapshot): Identity {
116 return new Identity(
117 IdentityId.from(snapshot.id),
118 Nickname.fromStorage(snapshot.nick),
119 NostrKeyPair.fromStorage(snapshot.privkey),
120 new Date(snapshot.createdAt)
121 );
122 }
123
124 // ─────────────────────────────────────────────────────────────────────────
125 // Getters (Read-only access to state)
126 // ─────────────────────────────────────────────────────────────────────────
127
128 get id(): IdentityId {
129 return this._id;
130 }
131
132 get nickname(): string {
133 return this._nickname.value;
134 }
135
136 get publicKey(): string {
137 return this._keyPair.publicKeyHex;
138 }
139
140 get npub(): string {
141 return this._keyPair.npub;
142 }
143
144 get nsec(): string {
145 return this._keyPair.nsec;
146 }
147
148 get createdAt(): Date {
149 return this._createdAt;
150 }
151
152 // ─────────────────────────────────────────────────────────────────────────
153 // Behavior Methods
154 // ─────────────────────────────────────────────────────────────────────────
155
156 /**
157 * Rename this identity.
158 *
159 * @param newNickname - The new nickname
160 * @throws InvalidNicknameError if nickname is invalid
161 */
162 rename(newNickname: string): void {
163 const oldNickname = this._nickname.value;
164 this._nickname = Nickname.create(newNickname);
165
166 this.addDomainEvent(
167 new IdentityRenamed(this._id.value, oldNickname, newNickname)
168 );
169 }
170
171 /**
172 * Sign a Nostr event with this identity's private key.
173 *
174 * @param event - The unsigned event template
175 * @param signFn - The signing function (injected to avoid coupling)
176 * @returns The signed event with id, pubkey, and sig
177 */
178 sign(event: UnsignedEvent, signFn: SigningFunction): SignedEvent {
179 const signedEvent = signFn(event, this._keyPair.getPrivateKeyBytes());
180
181 this.addDomainEvent(
182 new IdentitySigned(this._id.value, event.kind, signedEvent.id)
183 );
184
185 return signedEvent;
186 }
187
188 /**
189 * Encrypt a message using NIP-04 encryption.
190 *
191 * @param plaintext - The message to encrypt
192 * @param recipientPubkey - The recipient's public key (hex)
193 * @param encryptFn - The NIP-04 encryption function
194 */
195 async encryptNip04(
196 plaintext: string,
197 recipientPubkey: string,
198 encryptFn: EncryptFunction
199 ): Promise<string> {
200 return encryptFn(
201 this._keyPair.getPrivateKeyBytes(),
202 recipientPubkey,
203 plaintext
204 );
205 }
206
207 /**
208 * Decrypt a message using NIP-04 decryption.
209 *
210 * @param ciphertext - The encrypted message
211 * @param senderPubkey - The sender's public key (hex)
212 * @param decryptFn - The NIP-04 decryption function
213 */
214 async decryptNip04(
215 ciphertext: string,
216 senderPubkey: string,
217 decryptFn: DecryptFunction
218 ): Promise<string> {
219 return decryptFn(
220 this._keyPair.getPrivateKeyBytes(),
221 senderPubkey,
222 ciphertext
223 );
224 }
225
226 /**
227 * Encrypt a message using NIP-44 encryption.
228 *
229 * @param plaintext - The message to encrypt
230 * @param recipientPubkey - The recipient's public key (hex)
231 * @param encryptFn - The NIP-44 encryption function
232 */
233 async encryptNip44(
234 plaintext: string,
235 recipientPubkey: string,
236 encryptFn: EncryptFunction
237 ): Promise<string> {
238 return encryptFn(
239 this._keyPair.getPrivateKeyBytes(),
240 recipientPubkey,
241 plaintext
242 );
243 }
244
245 /**
246 * Decrypt a message using NIP-44 decryption.
247 *
248 * @param ciphertext - The encrypted message
249 * @param senderPubkey - The sender's public key (hex)
250 * @param decryptFn - The NIP-44 decryption function
251 */
252 async decryptNip44(
253 ciphertext: string,
254 senderPubkey: string,
255 decryptFn: DecryptFunction
256 ): Promise<string> {
257 return decryptFn(
258 this._keyPair.getPrivateKeyBytes(),
259 senderPubkey,
260 ciphertext
261 );
262 }
263
264 /**
265 * Check if this identity has the same private key as another.
266 * Used for duplicate detection.
267 */
268 hasSameKeyAs(other: Identity): boolean {
269 return this._keyPair.hasSamePublicKey(other._keyPair);
270 }
271
272 /**
273 * Check if this identity matches a given public key.
274 */
275 matchesPublicKey(publicKey: string): boolean {
276 return this._keyPair.matchesPublicKey(publicKey);
277 }
278
279 // ─────────────────────────────────────────────────────────────────────────
280 // Persistence
281 // ─────────────────────────────────────────────────────────────────────────
282
283 /**
284 * Convert to a snapshot for persistence.
285 */
286 toSnapshot(): IdentitySnapshot {
287 return {
288 id: this._id.value,
289 nick: this._nickname.value,
290 privkey: this._keyPair.toStorageHex(),
291 createdAt: this._createdAt.toISOString(),
292 };
293 }
294
295 // ─────────────────────────────────────────────────────────────────────────
296 // Equality
297 // ─────────────────────────────────────────────────────────────────────────
298
299 /**
300 * Check equality based on identity ID.
301 */
302 equals(other: Identity): boolean {
303 return this._id.equals(other._id);
304 }
305 }
306