yakmesh 1.3.1 β†’ 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mesh/annex.js ADDED
@@ -0,0 +1,743 @@
1
+ /**
2
+ * Yakmesh Annex - Autonomous Network Negotiated Encrypted eXchange
3
+ *
4
+ * End-to-end encrypted point-to-point communication between mesh peers using:
5
+ * - ML-KEM768 (Kyber) for quantum-resistant key encapsulation
6
+ * - AES-256-GCM for authenticated symmetric encryption
7
+ * - ML-DSA-65 signatures for sender authentication
8
+ * - Perfect forward secrecy via ephemeral key exchange
9
+ *
10
+ * Key Innovation: "Changes pass through math alone"
11
+ * - All payload transformations are cryptographically autonomous
12
+ * - No plaintext touches the network - only ciphertext
13
+ * - Recipients prove possession of private key to decrypt
14
+ * - Sovereignty over your data channel
15
+ *
16
+ * Use Cases:
17
+ * - Authenticated content delivery (vs public gossip)
18
+ * - Beacon acknowledgments with encryption
19
+ * - App-specific private payloads
20
+ * - Site/CDN authentication tokens
21
+ * - Direct peer-to-peer secure messaging
22
+ *
23
+ * @module mesh/annex
24
+ * @license MIT
25
+ * @copyright 2026 YAKMESHβ„’ Contributors
26
+ */
27
+
28
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
29
+ import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
30
+ import { sha3_256 } from '@noble/hashes/sha3.js';
31
+ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
32
+
33
+ const ANNEX_CONFIG = {
34
+ // Encryption
35
+ symmetricAlgorithm: 'aes-256-gcm',
36
+ nonceSize: 12,
37
+ authTagLength: 16,
38
+
39
+ // Key derivation
40
+ keyDerivationSalt: 'YAKMESH-ANNEX-2026',
41
+ contextSeparator: ':',
42
+
43
+ // Session management
44
+ sessionTimeout: 3600000, // 1 hour session lifetime
45
+ rekeyInterval: 300000, // Re-key every 5 minutes for PFS
46
+ maxMessagesPerKey: 10000, // Force re-key after N messages
47
+
48
+ // Message types
49
+ messageTypes: {
50
+ KEY_EXCHANGE: 'annex:key_exchange',
51
+ KEY_RESPONSE: 'annex:key_response',
52
+ ENCRYPTED: 'annex:encrypted',
53
+ REKEY: 'annex:rekey',
54
+ CLOSE: 'annex:close',
55
+ },
56
+ };
57
+
58
+ /**
59
+ * Encrypted message envelope - the quantum-sealed diplomatic pouch
60
+ */
61
+ class AnnexEnvelope {
62
+ constructor(options) {
63
+ this.id = options.id || bytesToHex(randomBytes(16));
64
+ this.type = options.type || ANNEX_CONFIG.messageTypes.ENCRYPTED;
65
+ this.senderId = options.senderId;
66
+ this.recipientId = options.recipientId;
67
+ this.sessionId = options.sessionId;
68
+ this.sequence = options.sequence || 0;
69
+ this.timestamp = options.timestamp || Date.now();
70
+
71
+ // Encrypted payload components
72
+ this.nonce = options.nonce || null;
73
+ this.ciphertext = options.ciphertext || null;
74
+ this.authTag = options.authTag || null;
75
+
76
+ // Key exchange components (only for key exchange messages)
77
+ this.kemCiphertext = options.kemCiphertext || null;
78
+ this.kemPublicKey = options.kemPublicKey || null;
79
+
80
+ // Signature (ML-DSA-65)
81
+ this.signature = options.signature || null;
82
+ }
83
+
84
+ /**
85
+ * Compute hash for signing
86
+ */
87
+ getSigningPayload() {
88
+ return JSON.stringify({
89
+ id: this.id,
90
+ type: this.type,
91
+ senderId: this.senderId,
92
+ recipientId: this.recipientId,
93
+ sessionId: this.sessionId,
94
+ sequence: this.sequence,
95
+ timestamp: this.timestamp,
96
+ nonce: this.nonce,
97
+ ciphertext: this.ciphertext,
98
+ authTag: this.authTag,
99
+ kemCiphertext: this.kemCiphertext,
100
+ kemPublicKey: this.kemPublicKey,
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Serialize for transport
106
+ */
107
+ toJSON() {
108
+ return {
109
+ id: this.id,
110
+ type: this.type,
111
+ senderId: this.senderId,
112
+ recipientId: this.recipientId,
113
+ sessionId: this.sessionId,
114
+ sequence: this.sequence,
115
+ timestamp: this.timestamp,
116
+ nonce: this.nonce,
117
+ ciphertext: this.ciphertext,
118
+ authTag: this.authTag,
119
+ kemCiphertext: this.kemCiphertext,
120
+ kemPublicKey: this.kemPublicKey,
121
+ signature: this.signature,
122
+ };
123
+ }
124
+
125
+ static fromJSON(data) {
126
+ return new AnnexEnvelope(data);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * An annexed territory - a secure session with a single peer
132
+ */
133
+ class AnnexSession {
134
+ constructor(options) {
135
+ this.sessionId = options.sessionId || bytesToHex(randomBytes(16));
136
+ this.localNodeId = options.localNodeId;
137
+ this.remoteNodeId = options.remoteNodeId;
138
+ this.initiator = options.initiator || false;
139
+
140
+ // Key material
141
+ this.kemKeyPair = null; // Our ephemeral KEM key pair
142
+ this.sharedSecret = null; // Derived shared secret
143
+ this.encryptionKey = null; // Current symmetric key
144
+ this.sendSequence = 0; // Outbound message counter
145
+ this.recvSequence = 0; // Inbound message counter
146
+ this.messageCount = 0; // Total messages with current key
147
+
148
+ // State
149
+ this.established = false;
150
+ this.createdAt = Date.now();
151
+ this.lastActivity = Date.now();
152
+ this.lastRekey = null;
153
+ }
154
+
155
+ /**
156
+ * Generate ephemeral KEM key pair for this session
157
+ */
158
+ generateKeyPair() {
159
+ const seed = randomBytes(64);
160
+ this.kemKeyPair = ml_kem768.keygen(seed);
161
+ return bytesToHex(this.kemKeyPair.publicKey);
162
+ }
163
+
164
+ /**
165
+ * Complete key exchange as initiator (encapsulate with peer's public key)
166
+ */
167
+ encapsulate(peerPublicKey) {
168
+ const publicKeyBytes = hexToBytes(peerPublicKey);
169
+ const result = ml_kem768.encapsulate(publicKeyBytes);
170
+
171
+ this.sharedSecret = result.sharedSecret;
172
+ this.encryptionKey = this._deriveEncryptionKey();
173
+ this.established = true;
174
+ this.lastRekey = Date.now();
175
+
176
+ return bytesToHex(result.ciphertext);
177
+ }
178
+
179
+ /**
180
+ * Complete key exchange as responder (decapsulate received ciphertext)
181
+ */
182
+ decapsulate(ciphertext) {
183
+ if (!this.kemKeyPair) {
184
+ throw new Error('No key pair generated');
185
+ }
186
+
187
+ const ciphertextBytes = hexToBytes(ciphertext);
188
+ this.sharedSecret = ml_kem768.decapsulate(ciphertextBytes, this.kemKeyPair.secretKey);
189
+ this.encryptionKey = this._deriveEncryptionKey();
190
+ this.established = true;
191
+ this.lastRekey = Date.now();
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Encrypt a message for this session
198
+ */
199
+ encrypt(plaintext) {
200
+ if (!this.established || !this.encryptionKey) {
201
+ throw new Error('Session not established');
202
+ }
203
+
204
+ const nonce = randomBytes(ANNEX_CONFIG.nonceSize);
205
+ const cipher = createCipheriv(
206
+ ANNEX_CONFIG.symmetricAlgorithm,
207
+ this.encryptionKey,
208
+ nonce,
209
+ { authTagLength: ANNEX_CONFIG.authTagLength }
210
+ );
211
+
212
+ // Include sequence number in AAD for replay protection
213
+ const aad = Buffer.from(`${this.sessionId}:${this.sendSequence}`);
214
+ cipher.setAAD(aad);
215
+
216
+ const data = typeof plaintext === 'string' ? plaintext : JSON.stringify(plaintext);
217
+ const encrypted = Buffer.concat([
218
+ cipher.update(data, 'utf8'),
219
+ cipher.final(),
220
+ ]);
221
+
222
+ this.sendSequence++;
223
+ this.messageCount++;
224
+ this.lastActivity = Date.now();
225
+
226
+ return {
227
+ nonce: nonce.toString('hex'),
228
+ ciphertext: encrypted.toString('hex'),
229
+ authTag: cipher.getAuthTag().toString('hex'),
230
+ sequence: this.sendSequence - 1,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Decrypt a message for this session
236
+ */
237
+ decrypt(encryptedData, expectedSequence) {
238
+ if (!this.established || !this.encryptionKey) {
239
+ throw new Error('Session not established');
240
+ }
241
+
242
+ // Replay protection: sequence must be greater than last received
243
+ if (expectedSequence <= this.recvSequence && this.recvSequence > 0) {
244
+ throw new Error(`Replay detected: sequence ${expectedSequence} <= ${this.recvSequence}`);
245
+ }
246
+
247
+ const nonce = Buffer.from(encryptedData.nonce, 'hex');
248
+ const ciphertext = Buffer.from(encryptedData.ciphertext, 'hex');
249
+ const authTag = Buffer.from(encryptedData.authTag, 'hex');
250
+
251
+ const decipher = createDecipheriv(
252
+ ANNEX_CONFIG.symmetricAlgorithm,
253
+ this.encryptionKey,
254
+ nonce,
255
+ { authTagLength: ANNEX_CONFIG.authTagLength }
256
+ );
257
+
258
+ // Verify AAD
259
+ const aad = Buffer.from(`${this.sessionId}:${expectedSequence}`);
260
+ decipher.setAAD(aad);
261
+ decipher.setAuthTag(authTag);
262
+
263
+ const decrypted = Buffer.concat([
264
+ decipher.update(ciphertext),
265
+ decipher.final(),
266
+ ]);
267
+
268
+ this.recvSequence = expectedSequence;
269
+ this.lastActivity = Date.now();
270
+
271
+ return decrypted.toString('utf8');
272
+ }
273
+
274
+ /**
275
+ * Check if session needs re-keying for perfect forward secrecy
276
+ */
277
+ needsRekey() {
278
+ if (!this.established) return false;
279
+
280
+ const timeSinceRekey = Date.now() - (this.lastRekey || this.createdAt);
281
+ return (
282
+ timeSinceRekey > ANNEX_CONFIG.rekeyInterval ||
283
+ this.messageCount >= ANNEX_CONFIG.maxMessagesPerKey
284
+ );
285
+ }
286
+
287
+ /**
288
+ * Check if session has expired
289
+ */
290
+ isExpired() {
291
+ return Date.now() - this.lastActivity > ANNEX_CONFIG.sessionTimeout;
292
+ }
293
+
294
+ /**
295
+ * Derive symmetric encryption key from shared secret
296
+ */
297
+ _deriveEncryptionKey() {
298
+ return createHash('sha256')
299
+ .update(this.sharedSecret)
300
+ .update(ANNEX_CONFIG.keyDerivationSalt)
301
+ .update(this.sessionId)
302
+ .update(this.localNodeId)
303
+ .update(this.remoteNodeId)
304
+ .digest();
305
+ }
306
+ }
307
+
308
+ /**
309
+ * ANNEX - Autonomous Network Negotiated Encrypted eXchange
310
+ *
311
+ * Manages encrypted point-to-point communication channels
312
+ * "Annex your own sovereign data territory"
313
+ */
314
+ export class Annex {
315
+ constructor(options) {
316
+ this.identity = options.identity;
317
+ this.mesh = options.mesh;
318
+ this.sessions = new Map(); // remoteNodeId -> AnnexSession
319
+ this.pendingHandshakes = new Map();
320
+ this.messageHandlers = new Map();
321
+
322
+ // Stats
323
+ this.stats = {
324
+ sessionsCreated: 0,
325
+ messagesEncrypted: 0,
326
+ messagesDecrypted: 0,
327
+ handshakesFailed: 0,
328
+ replaysBlocked: 0,
329
+ };
330
+
331
+ // Register mesh handler for ANNEX messages
332
+ if (this.mesh) {
333
+ this._registerMeshHandlers();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Initialize or get secure session with a peer (annex territory)
339
+ */
340
+ async openChannel(remoteNodeId) {
341
+ // Check for existing session
342
+ let session = this.sessions.get(remoteNodeId);
343
+ if (session && session.established && !session.isExpired()) {
344
+ return session;
345
+ }
346
+
347
+ // Create new session
348
+ session = new AnnexSession({
349
+ localNodeId: this.identity.identity.nodeId,
350
+ remoteNodeId,
351
+ initiator: true,
352
+ });
353
+
354
+ // Generate our key pair
355
+ const ourPublicKey = session.generateKeyPair();
356
+
357
+ // Store pending handshake
358
+ this.pendingHandshakes.set(remoteNodeId, session);
359
+
360
+ // Send key exchange request
361
+ const envelope = new AnnexEnvelope({
362
+ type: ANNEX_CONFIG.messageTypes.KEY_EXCHANGE,
363
+ senderId: this.identity.identity.nodeId,
364
+ recipientId: remoteNodeId,
365
+ sessionId: session.sessionId,
366
+ kemPublicKey: ourPublicKey,
367
+ });
368
+
369
+ // Sign the envelope
370
+ envelope.signature = this.identity.sign(envelope.getSigningPayload());
371
+
372
+ // Send via mesh
373
+ await this._sendToMesh(remoteNodeId, envelope);
374
+
375
+ // Wait for response (with timeout)
376
+ return new Promise((resolve, reject) => {
377
+ const timeout = setTimeout(() => {
378
+ this.pendingHandshakes.delete(remoteNodeId);
379
+ this.stats.handshakesFailed++;
380
+ reject(new Error('ANNEX handshake timeout'));
381
+ }, 30000);
382
+
383
+ session._resolveHandshake = (establishedSession) => {
384
+ clearTimeout(timeout);
385
+ resolve(establishedSession);
386
+ };
387
+
388
+ session._rejectHandshake = (error) => {
389
+ clearTimeout(timeout);
390
+ this.stats.handshakesFailed++;
391
+ reject(error);
392
+ };
393
+ });
394
+ }
395
+
396
+ /**
397
+ * Send an encrypted message through the ANNEX channel
398
+ */
399
+ async send(remoteNodeId, payload, options = {}) {
400
+ // Ensure we have a session
401
+ let session = this.sessions.get(remoteNodeId);
402
+ if (!session || !session.established || session.isExpired()) {
403
+ session = await this.openChannel(remoteNodeId);
404
+ }
405
+
406
+ // Check for re-key need
407
+ if (session.needsRekey()) {
408
+ await this._rekey(session);
409
+ }
410
+
411
+ // Encrypt the payload
412
+ const encrypted = session.encrypt(payload);
413
+
414
+ // Create envelope
415
+ const envelope = new AnnexEnvelope({
416
+ type: ANNEX_CONFIG.messageTypes.ENCRYPTED,
417
+ senderId: this.identity.identity.nodeId,
418
+ recipientId: remoteNodeId,
419
+ sessionId: session.sessionId,
420
+ sequence: encrypted.sequence,
421
+ nonce: encrypted.nonce,
422
+ ciphertext: encrypted.ciphertext,
423
+ authTag: encrypted.authTag,
424
+ });
425
+
426
+ // Sign
427
+ envelope.signature = this.identity.sign(envelope.getSigningPayload());
428
+
429
+ // Send
430
+ await this._sendToMesh(remoteNodeId, envelope);
431
+ this.stats.messagesEncrypted++;
432
+
433
+ return { sent: true, sessionId: session.sessionId, sequence: encrypted.sequence };
434
+ }
435
+
436
+ /**
437
+ * Register handler for decrypted messages
438
+ */
439
+ onMessage(handler) {
440
+ const id = bytesToHex(randomBytes(8));
441
+ this.messageHandlers.set(id, handler);
442
+ return id;
443
+ }
444
+
445
+ /**
446
+ * Remove message handler
447
+ */
448
+ offMessage(handlerId) {
449
+ this.messageHandlers.delete(handlerId);
450
+ }
451
+
452
+ /**
453
+ * Close ANNEX channel with peer (release territory)
454
+ */
455
+ async closeChannel(remoteNodeId) {
456
+ const session = this.sessions.get(remoteNodeId);
457
+ if (!session) return;
458
+
459
+ // Send close notification
460
+ const envelope = new AnnexEnvelope({
461
+ type: ANNEX_CONFIG.messageTypes.CLOSE,
462
+ senderId: this.identity.identity.nodeId,
463
+ recipientId: remoteNodeId,
464
+ sessionId: session.sessionId,
465
+ });
466
+ envelope.signature = this.identity.sign(envelope.getSigningPayload());
467
+
468
+ await this._sendToMesh(remoteNodeId, envelope);
469
+ this.sessions.delete(remoteNodeId);
470
+ }
471
+
472
+ /**
473
+ * Get session info
474
+ */
475
+ getSessionInfo(remoteNodeId) {
476
+ const session = this.sessions.get(remoteNodeId);
477
+ if (!session) return null;
478
+
479
+ return {
480
+ sessionId: session.sessionId,
481
+ established: session.established,
482
+ createdAt: session.createdAt,
483
+ lastActivity: session.lastActivity,
484
+ messageCount: session.messageCount,
485
+ needsRekey: session.needsRekey(),
486
+ isExpired: session.isExpired(),
487
+ };
488
+ }
489
+
490
+ /**
491
+ * List all active annexes (sessions)
492
+ */
493
+ listAnnexes() {
494
+ const annexes = [];
495
+ for (const [nodeId, session] of this.sessions) {
496
+ annexes.push({
497
+ nodeId,
498
+ sessionId: session.sessionId,
499
+ established: session.established,
500
+ createdAt: session.createdAt,
501
+ lastActivity: session.lastActivity,
502
+ messageCount: session.messageCount,
503
+ isExpired: session.isExpired(),
504
+ });
505
+ }
506
+ return annexes;
507
+ }
508
+
509
+ /**
510
+ * Get stats
511
+ */
512
+ getStats() {
513
+ return {
514
+ ...this.stats,
515
+ activeSessions: this.sessions.size,
516
+ pendingHandshakes: this.pendingHandshakes.size,
517
+ };
518
+ }
519
+
520
+ // === Private Methods ===
521
+
522
+ _registerMeshHandlers() {
523
+ // Handle incoming ANNEX messages
524
+ this.mesh.on('annex', async (data, origin) => {
525
+ await this._handleAnnexMessage(data, origin);
526
+ });
527
+ }
528
+
529
+ async _handleAnnexMessage(envelope, origin) {
530
+ try {
531
+ // Verify signature
532
+ const sigPayload = AnnexEnvelope.fromJSON(envelope).getSigningPayload();
533
+ const peerPublicKey = this._getPeerPublicKey(envelope.senderId);
534
+
535
+ if (peerPublicKey && !this.identity.verify(sigPayload, envelope.signature, peerPublicKey)) {
536
+ console.warn(`⚠️ ANNEX: Invalid signature from ${envelope.senderId?.slice(0, 16)}...`);
537
+ return;
538
+ }
539
+
540
+ switch (envelope.type) {
541
+ case ANNEX_CONFIG.messageTypes.KEY_EXCHANGE:
542
+ await this._handleKeyExchange(envelope);
543
+ break;
544
+
545
+ case ANNEX_CONFIG.messageTypes.KEY_RESPONSE:
546
+ await this._handleKeyResponse(envelope);
547
+ break;
548
+
549
+ case ANNEX_CONFIG.messageTypes.ENCRYPTED:
550
+ await this._handleEncrypted(envelope);
551
+ break;
552
+
553
+ case ANNEX_CONFIG.messageTypes.REKEY:
554
+ await this._handleRekey(envelope);
555
+ break;
556
+
557
+ case ANNEX_CONFIG.messageTypes.CLOSE:
558
+ this.sessions.delete(envelope.senderId);
559
+ console.log(`🏳️ ANNEX: Channel closed by ${envelope.senderId?.slice(0, 16)}...`);
560
+ break;
561
+ }
562
+ } catch (err) {
563
+ console.error('ANNEX error:', err.message);
564
+ }
565
+ }
566
+
567
+ async _handleKeyExchange(envelope) {
568
+ console.log(`🀝 ANNEX: Key exchange from ${envelope.senderId?.slice(0, 16)}...`);
569
+
570
+ // Create responding session
571
+ const session = new AnnexSession({
572
+ sessionId: envelope.sessionId,
573
+ localNodeId: this.identity.identity.nodeId,
574
+ remoteNodeId: envelope.senderId,
575
+ initiator: false,
576
+ });
577
+
578
+ // Generate our key pair and encapsulate with peer's public key
579
+ session.generateKeyPair();
580
+ const kemCiphertext = session.encapsulate(envelope.kemPublicKey);
581
+
582
+ // Store session
583
+ this.sessions.set(envelope.senderId, session);
584
+ this.stats.sessionsCreated++;
585
+
586
+ // Send response with our public key and the KEM ciphertext
587
+ const response = new AnnexEnvelope({
588
+ type: ANNEX_CONFIG.messageTypes.KEY_RESPONSE,
589
+ senderId: this.identity.identity.nodeId,
590
+ recipientId: envelope.senderId,
591
+ sessionId: session.sessionId,
592
+ kemPublicKey: bytesToHex(session.kemKeyPair.publicKey),
593
+ kemCiphertext: kemCiphertext,
594
+ });
595
+
596
+ response.signature = this.identity.sign(response.getSigningPayload());
597
+ await this._sendToMesh(envelope.senderId, response);
598
+
599
+ console.log(`πŸ” ANNEX: Channel established with ${envelope.senderId?.slice(0, 16)}...`);
600
+ }
601
+
602
+ async _handleKeyResponse(envelope) {
603
+ const session = this.pendingHandshakes.get(envelope.senderId);
604
+ if (!session) {
605
+ console.warn('ANNEX: Unexpected key response');
606
+ return;
607
+ }
608
+
609
+ // Decapsulate to get shared secret
610
+ session.decapsulate(envelope.kemCiphertext);
611
+
612
+ // Move to active sessions
613
+ this.pendingHandshakes.delete(envelope.senderId);
614
+ this.sessions.set(envelope.senderId, session);
615
+ this.stats.sessionsCreated++;
616
+
617
+ console.log(`πŸ” ANNEX: Channel established with ${envelope.senderId?.slice(0, 16)}...`);
618
+
619
+ // Resolve the handshake promise
620
+ if (session._resolveHandshake) {
621
+ session._resolveHandshake(session);
622
+ }
623
+ }
624
+
625
+ async _handleEncrypted(envelope) {
626
+ const session = this.sessions.get(envelope.senderId);
627
+ if (!session || !session.established) {
628
+ console.warn('ANNEX: No session for encrypted message');
629
+ return;
630
+ }
631
+
632
+ try {
633
+ // Decrypt
634
+ const plaintext = session.decrypt(
635
+ {
636
+ nonce: envelope.nonce,
637
+ ciphertext: envelope.ciphertext,
638
+ authTag: envelope.authTag,
639
+ },
640
+ envelope.sequence
641
+ );
642
+
643
+ this.stats.messagesDecrypted++;
644
+
645
+ // Parse and dispatch to handlers
646
+ let payload;
647
+ try {
648
+ payload = JSON.parse(plaintext);
649
+ } catch {
650
+ payload = plaintext;
651
+ }
652
+
653
+ // Dispatch to handlers
654
+ for (const handler of this.messageHandlers.values()) {
655
+ try {
656
+ await handler({
657
+ from: envelope.senderId,
658
+ sessionId: envelope.sessionId,
659
+ payload,
660
+ timestamp: envelope.timestamp,
661
+ });
662
+ } catch (err) {
663
+ console.error('ANNEX handler error:', err.message);
664
+ }
665
+ }
666
+ } catch (err) {
667
+ if (err.message.includes('Replay')) {
668
+ this.stats.replaysBlocked++;
669
+ console.warn(`⚠️ ANNEX: ${err.message}`);
670
+ } else {
671
+ throw err;
672
+ }
673
+ }
674
+ }
675
+
676
+ async _handleRekey(envelope) {
677
+ const session = this.sessions.get(envelope.senderId);
678
+ if (!session) return;
679
+
680
+ console.log(`πŸ”„ ANNEX: Re-keying with ${envelope.senderId?.slice(0, 16)}...`);
681
+
682
+ // Respond to re-key with new key exchange
683
+ session.generateKeyPair();
684
+ const kemCiphertext = session.encapsulate(envelope.kemPublicKey);
685
+ session.messageCount = 0;
686
+
687
+ const response = new AnnexEnvelope({
688
+ type: ANNEX_CONFIG.messageTypes.KEY_RESPONSE,
689
+ senderId: this.identity.identity.nodeId,
690
+ recipientId: envelope.senderId,
691
+ sessionId: session.sessionId,
692
+ kemPublicKey: bytesToHex(session.kemKeyPair.publicKey),
693
+ kemCiphertext,
694
+ });
695
+
696
+ response.signature = this.identity.sign(response.getSigningPayload());
697
+ await this._sendToMesh(envelope.senderId, response);
698
+ }
699
+
700
+ async _rekey(session) {
701
+ console.log(`πŸ”„ ANNEX: Initiating re-key with ${session.remoteNodeId?.slice(0, 16)}...`);
702
+
703
+ // Generate new ephemeral keys
704
+ const newPublicKey = session.generateKeyPair();
705
+ session.messageCount = 0;
706
+
707
+ const envelope = new AnnexEnvelope({
708
+ type: ANNEX_CONFIG.messageTypes.REKEY,
709
+ senderId: this.identity.identity.nodeId,
710
+ recipientId: session.remoteNodeId,
711
+ sessionId: session.sessionId,
712
+ kemPublicKey: newPublicKey,
713
+ });
714
+
715
+ envelope.signature = this.identity.sign(envelope.getSigningPayload());
716
+ await this._sendToMesh(session.remoteNodeId, envelope);
717
+ }
718
+
719
+ async _sendToMesh(remoteNodeId, envelope) {
720
+ if (!this.mesh) {
721
+ throw new Error('No mesh connection');
722
+ }
723
+
724
+ this.mesh.sendTo(remoteNodeId, {
725
+ type: 'annex',
726
+ annex: envelope.toJSON(),
727
+ });
728
+ }
729
+
730
+ _getPeerPublicKey(nodeId) {
731
+ // Get from mesh peer info
732
+ if (this.mesh && this.mesh.peers) {
733
+ const peer = this.mesh.peers.get(nodeId);
734
+ return peer?.identity?.publicKey || null;
735
+ }
736
+ return null;
737
+ }
738
+ }
739
+
740
+ // Export config for external use
741
+ export { ANNEX_CONFIG, AnnexEnvelope, AnnexSession };
742
+
743
+ export default Annex;