bitchat-node 0.1.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.
Files changed (102) hide show
  1. package/README.md +223 -0
  2. package/dist/bin/bitchat.d.ts +7 -0
  3. package/dist/bin/bitchat.d.ts.map +1 -0
  4. package/dist/bin/bitchat.js +69 -0
  5. package/dist/bin/bitchat.js.map +1 -0
  6. package/dist/client.d.ts +77 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +411 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/crypto/index.d.ts +6 -0
  11. package/dist/crypto/index.d.ts.map +1 -0
  12. package/dist/crypto/index.js +6 -0
  13. package/dist/crypto/index.js.map +1 -0
  14. package/dist/crypto/noise.d.ts +72 -0
  15. package/dist/crypto/noise.d.ts.map +1 -0
  16. package/dist/crypto/noise.js +470 -0
  17. package/dist/crypto/noise.js.map +1 -0
  18. package/dist/crypto/signing.d.ts +34 -0
  19. package/dist/crypto/signing.d.ts.map +1 -0
  20. package/dist/crypto/signing.js +56 -0
  21. package/dist/crypto/signing.js.map +1 -0
  22. package/dist/index.d.ts +32 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +48 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/mesh/deduplicator.d.ts +48 -0
  27. package/dist/mesh/deduplicator.d.ts.map +1 -0
  28. package/dist/mesh/deduplicator.js +107 -0
  29. package/dist/mesh/deduplicator.js.map +1 -0
  30. package/dist/mesh/index.d.ts +6 -0
  31. package/dist/mesh/index.d.ts.map +1 -0
  32. package/dist/mesh/index.js +6 -0
  33. package/dist/mesh/index.js.map +1 -0
  34. package/dist/mesh/router.d.ts +90 -0
  35. package/dist/mesh/router.d.ts.map +1 -0
  36. package/dist/mesh/router.js +204 -0
  37. package/dist/mesh/router.js.map +1 -0
  38. package/dist/protocol/binary.d.ts +37 -0
  39. package/dist/protocol/binary.d.ts.map +1 -0
  40. package/dist/protocol/binary.js +310 -0
  41. package/dist/protocol/binary.js.map +1 -0
  42. package/dist/protocol/constants.d.ts +30 -0
  43. package/dist/protocol/constants.d.ts.map +1 -0
  44. package/dist/protocol/constants.js +37 -0
  45. package/dist/protocol/constants.js.map +1 -0
  46. package/dist/protocol/index.d.ts +8 -0
  47. package/dist/protocol/index.d.ts.map +1 -0
  48. package/dist/protocol/index.js +8 -0
  49. package/dist/protocol/index.js.map +1 -0
  50. package/dist/protocol/packets.d.ts +38 -0
  51. package/dist/protocol/packets.d.ts.map +1 -0
  52. package/dist/protocol/packets.js +177 -0
  53. package/dist/protocol/packets.js.map +1 -0
  54. package/dist/protocol/types.d.ts +134 -0
  55. package/dist/protocol/types.d.ts.map +1 -0
  56. package/dist/protocol/types.js +108 -0
  57. package/dist/protocol/types.js.map +1 -0
  58. package/dist/session/index.d.ts +5 -0
  59. package/dist/session/index.d.ts.map +1 -0
  60. package/dist/session/index.js +5 -0
  61. package/dist/session/index.js.map +1 -0
  62. package/dist/session/manager.d.ts +113 -0
  63. package/dist/session/manager.d.ts.map +1 -0
  64. package/dist/session/manager.js +371 -0
  65. package/dist/session/manager.js.map +1 -0
  66. package/dist/transport/ble.d.ts +92 -0
  67. package/dist/transport/ble.d.ts.map +1 -0
  68. package/dist/transport/ble.js +434 -0
  69. package/dist/transport/ble.js.map +1 -0
  70. package/dist/transport/index.d.ts +5 -0
  71. package/dist/transport/index.d.ts.map +1 -0
  72. package/dist/transport/index.js +5 -0
  73. package/dist/transport/index.js.map +1 -0
  74. package/dist/ui/index.d.ts +2 -0
  75. package/dist/ui/index.d.ts.map +1 -0
  76. package/dist/ui/index.js +2 -0
  77. package/dist/ui/index.js.map +1 -0
  78. package/dist/ui/server.d.ts +16 -0
  79. package/dist/ui/server.d.ts.map +1 -0
  80. package/dist/ui/server.js +510 -0
  81. package/dist/ui/server.js.map +1 -0
  82. package/package.json +79 -0
  83. package/src/bin/bitchat.ts +87 -0
  84. package/src/client.ts +519 -0
  85. package/src/crypto/index.ts +22 -0
  86. package/src/crypto/noise.ts +574 -0
  87. package/src/crypto/signing.ts +66 -0
  88. package/src/index.ts +95 -0
  89. package/src/mesh/deduplicator.ts +129 -0
  90. package/src/mesh/index.ts +6 -0
  91. package/src/mesh/router.ts +258 -0
  92. package/src/protocol/binary.ts +345 -0
  93. package/src/protocol/constants.ts +43 -0
  94. package/src/protocol/index.ts +15 -0
  95. package/src/protocol/packets.ts +223 -0
  96. package/src/protocol/types.ts +182 -0
  97. package/src/session/index.ts +9 -0
  98. package/src/session/manager.ts +476 -0
  99. package/src/transport/ble.ts +553 -0
  100. package/src/transport/index.ts +10 -0
  101. package/src/ui/index.ts +1 -0
  102. package/src/ui/server.ts +569 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Bitchat Protocol Types
3
+ * From: bitchat/Protocols/BitchatProtocol.swift
4
+ * bitchat/Models/BitchatPacket.swift
5
+ */
6
+
7
+ import { hash as sha256 } from '@stablelib/sha256';
8
+ import { SENDER_ID_SIZE } from './constants.js';
9
+
10
+ /**
11
+ * Message types from BitchatProtocol.swift
12
+ * Reduced from 24 types to 8 essential ones.
13
+ */
14
+ export enum MessageType {
15
+ // Public messages (unencrypted)
16
+ ANNOUNCE = 0x01, // "I'm here" with nickname
17
+ MESSAGE = 0x02, // Public chat message
18
+ LEAVE = 0x03, // "I'm leaving"
19
+ REQUEST_SYNC = 0x21, // GCS filter-based sync request
20
+
21
+ // Noise encryption
22
+ NOISE_HANDSHAKE = 0x10, // Handshake (init or response)
23
+ NOISE_ENCRYPTED = 0x11, // All encrypted payloads
24
+
25
+ // Fragmentation
26
+ FRAGMENT = 0x20, // Fragment for large messages
27
+ FILE_TRANSFER = 0x22, // Binary file/audio/image
28
+ }
29
+
30
+ /**
31
+ * Noise payload types (first byte of decrypted payload)
32
+ */
33
+ export enum NoisePayloadType {
34
+ PRIVATE_MESSAGE = 0x01,
35
+ READ_RECEIPT = 0x02,
36
+ DELIVERED = 0x03,
37
+ VERIFY_CHALLENGE = 0x10,
38
+ VERIFY_RESPONSE = 0x11,
39
+ }
40
+
41
+ /**
42
+ * Binary protocol flags
43
+ */
44
+ export const PacketFlags = {
45
+ HAS_RECIPIENT: 0x01,
46
+ HAS_SIGNATURE: 0x02,
47
+ IS_COMPRESSED: 0x04,
48
+ HAS_ROUTE: 0x08, // v2+ only
49
+ IS_RSR: 0x10, // Request-Sync-Response
50
+ } as const;
51
+
52
+ /**
53
+ * Peer identifier (8 bytes, displayed as 16-char hex)
54
+ */
55
+ export class PeerID {
56
+ private readonly bytes: Uint8Array;
57
+
58
+ constructor(bytes: Uint8Array | string) {
59
+ if (typeof bytes === 'string') {
60
+ // Parse hex string
61
+ const hex = bytes.replace(/^0x/, '').toLowerCase();
62
+ const padded = hex.padStart(16, '0').slice(0, 16);
63
+ this.bytes = new Uint8Array(SENDER_ID_SIZE);
64
+ for (let i = 0; i < SENDER_ID_SIZE; i++) {
65
+ this.bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
66
+ }
67
+ } else {
68
+ this.bytes = new Uint8Array(SENDER_ID_SIZE);
69
+ this.bytes.set(bytes.subarray(0, SENDER_ID_SIZE));
70
+ }
71
+ }
72
+
73
+ static fromPublicKey(publicKey: Uint8Array): PeerID {
74
+ // SHA256 of public key, first 8 bytes
75
+ const hash = sha256(publicKey);
76
+ return new PeerID(hash.subarray(0, SENDER_ID_SIZE));
77
+ }
78
+
79
+ toBytes(): Uint8Array {
80
+ return new Uint8Array(this.bytes);
81
+ }
82
+
83
+ toHex(): string {
84
+ return Array.from(this.bytes)
85
+ .map((b) => b.toString(16).padStart(2, '0'))
86
+ .join('');
87
+ }
88
+
89
+ toString(): string {
90
+ return this.toHex();
91
+ }
92
+
93
+ equals(other: PeerID): boolean {
94
+ if (this.bytes.length !== other.bytes.length) return false;
95
+ for (let i = 0; i < this.bytes.length; i++) {
96
+ if (this.bytes[i] !== other.bytes[i]) return false;
97
+ }
98
+ return true;
99
+ }
100
+
101
+ get isEmpty(): boolean {
102
+ return this.bytes.every((b) => b === 0);
103
+ }
104
+
105
+ static readonly BROADCAST = new PeerID(new Uint8Array(SENDER_ID_SIZE).fill(0xff));
106
+ }
107
+
108
+ /**
109
+ * Core packet structure
110
+ * From: bitchat/Models/BitchatPacket.swift
111
+ */
112
+ export interface BitchatPacket {
113
+ version: number; // Protocol version (1 or 2)
114
+ type: MessageType;
115
+ ttl: number; // Time-to-live for mesh routing
116
+ timestamp: bigint; // Milliseconds since epoch
117
+ senderID: PeerID;
118
+ recipientID?: PeerID; // Optional for directed messages
119
+ payload: Uint8Array;
120
+ signature?: Uint8Array; // Ed25519 signature
121
+ route?: PeerID[]; // Source routing (v2+)
122
+ isRSR: boolean; // Request-Sync-Response flag
123
+ }
124
+
125
+ /**
126
+ * Delivery status for messages
127
+ */
128
+ export type DeliveryStatus =
129
+ | { type: 'sending' }
130
+ | { type: 'sent' }
131
+ | { type: 'delivered'; to: PeerID; at: Date }
132
+ | { type: 'read'; by: PeerID; at: Date }
133
+ | { type: 'failed'; reason: string };
134
+
135
+ /**
136
+ * Handshake state for lazy Noise sessions
137
+ */
138
+ export enum HandshakeState {
139
+ NONE = 'none',
140
+ QUEUED = 'queued',
141
+ HANDSHAKING = 'handshaking',
142
+ ESTABLISHED = 'established',
143
+ FAILED = 'failed',
144
+ }
145
+
146
+ /**
147
+ * Connected peer information
148
+ */
149
+ export interface PeerInfo {
150
+ peerID: PeerID;
151
+ nickname: string;
152
+ isConnected: boolean;
153
+ noisePublicKey?: Uint8Array;
154
+ signingPublicKey?: Uint8Array;
155
+ isVerified: boolean;
156
+ lastSeen: Date;
157
+ handshakeState: HandshakeState;
158
+ }
159
+
160
+ /**
161
+ * Chat message (application layer)
162
+ */
163
+ export interface ChatMessage {
164
+ id: string;
165
+ sender: PeerID;
166
+ senderNickname: string;
167
+ content: string;
168
+ timestamp: Date;
169
+ isPrivate: boolean;
170
+ recipientNickname?: string;
171
+ originalSender?: PeerID; // For relayed messages
172
+ deliveryStatus: DeliveryStatus;
173
+ }
174
+
175
+ /**
176
+ * Read receipt
177
+ */
178
+ export interface ReadReceipt {
179
+ originalMessageID: string;
180
+ readBy: PeerID;
181
+ readAt: Date;
182
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Session module exports
3
+ */
4
+
5
+ export {
6
+ SessionManager,
7
+ type SessionManagerConfig,
8
+ type SessionManagerEvents,
9
+ } from './manager.js';
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Session Manager
3
+ * Manages Noise sessions with peers, handling handshakes and encryption
4
+ */
5
+
6
+ import { EventEmitter } from 'node:events';
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import {
11
+ generateKeyPair,
12
+ HandshakeState as NoiseHandshake,
13
+ type NoiseKeyPair,
14
+ type NoiseRole,
15
+ NoiseSession,
16
+ } from '../crypto/noise.js';
17
+ import { fingerprint, generateSigningKeyPair, type SigningKeyPair } from '../crypto/signing.js';
18
+ import { HandshakeState, PeerID, type PeerInfo } from '../protocol/types.js';
19
+
20
+ // Key persistence path
21
+ const KEY_DIR = join(homedir(), '.bitchat-node');
22
+ const KEY_FILE = join(KEY_DIR, 'identity.json');
23
+
24
+ interface PersistedKeys {
25
+ noisePublicKey: string;
26
+ noiseSecretKey: string;
27
+ signingPublicKey: string;
28
+ signingSecretKey: string;
29
+ }
30
+
31
+ function loadOrCreateKeys(): { static: NoiseKeyPair; signing: SigningKeyPair } {
32
+ try {
33
+ if (existsSync(KEY_FILE)) {
34
+ const data = JSON.parse(readFileSync(KEY_FILE, 'utf-8')) as PersistedKeys;
35
+ console.log('[Keys] Loaded persistent identity from', KEY_FILE);
36
+ return {
37
+ static: {
38
+ publicKey: Buffer.from(data.noisePublicKey, 'hex'),
39
+ secretKey: Buffer.from(data.noiseSecretKey, 'hex'),
40
+ },
41
+ signing: {
42
+ publicKey: Buffer.from(data.signingPublicKey, 'hex'),
43
+ secretKey: Buffer.from(data.signingSecretKey, 'hex'),
44
+ },
45
+ };
46
+ }
47
+ } catch (e) {
48
+ console.log('[Keys] Failed to load keys, generating new ones:', (e as Error).message);
49
+ }
50
+
51
+ // Generate new keys
52
+ const staticKP = generateKeyPair();
53
+ const signingKP = generateSigningKeyPair();
54
+
55
+ // Persist them
56
+ try {
57
+ mkdirSync(KEY_DIR, { recursive: true });
58
+ const data: PersistedKeys = {
59
+ noisePublicKey: Buffer.from(staticKP.publicKey).toString('hex'),
60
+ noiseSecretKey: Buffer.from(staticKP.secretKey).toString('hex'),
61
+ signingPublicKey: Buffer.from(signingKP.publicKey).toString('hex'),
62
+ signingSecretKey: Buffer.from(signingKP.secretKey).toString('hex'),
63
+ };
64
+ writeFileSync(KEY_FILE, JSON.stringify(data, null, 2));
65
+ console.log('[Keys] Generated and saved new identity to', KEY_FILE);
66
+ } catch (e) {
67
+ console.log('[Keys] Warning: Failed to persist keys:', (e as Error).message);
68
+ }
69
+
70
+ return { static: staticKP, signing: signingKP };
71
+ }
72
+
73
+ export interface SessionManagerConfig {
74
+ autoHandshake: boolean;
75
+ handshakeTimeoutMs: number;
76
+ }
77
+
78
+ const DEFAULT_CONFIG: SessionManagerConfig = {
79
+ autoHandshake: true,
80
+ handshakeTimeoutMs: 30000,
81
+ };
82
+
83
+ interface PendingHandshake {
84
+ state: NoiseHandshake;
85
+ role: NoiseRole;
86
+ startedAt: number;
87
+ timeoutHandle?: ReturnType<typeof setTimeout>;
88
+ }
89
+
90
+ interface EstablishedSession {
91
+ session: NoiseSession;
92
+ establishedAt: number;
93
+ remotePublicKey: Uint8Array;
94
+ }
95
+
96
+ export interface SessionManagerEvents {
97
+ 'session:established': (peerID: PeerID, session: NoiseSession) => void;
98
+ 'session:failed': (peerID: PeerID, error: Error) => void;
99
+ 'session:closed': (peerID: PeerID) => void;
100
+ 'handshake:message': (peerID: PeerID, message: Uint8Array) => void;
101
+ }
102
+
103
+ /**
104
+ * Manages Noise sessions with peers
105
+ */
106
+ export class SessionManager extends EventEmitter {
107
+ private readonly config: SessionManagerConfig;
108
+
109
+ // Our keys
110
+ readonly staticKeyPair: NoiseKeyPair;
111
+ readonly signingKeyPair: SigningKeyPair;
112
+ readonly myPeerID: PeerID;
113
+ readonly fingerprint: string;
114
+
115
+ // Session state
116
+ private pendingHandshakes = new Map<string, PendingHandshake>();
117
+ private sessions = new Map<string, EstablishedSession>();
118
+
119
+ // Peer info
120
+ private peers = new Map<string, PeerInfo>();
121
+
122
+ // Messages queued for after handshake completion
123
+ private pendingMessages = new Map<string, Array<{ data: Uint8Array; timestamp: number }>>();
124
+
125
+ constructor(
126
+ config: Partial<SessionManagerConfig> = {},
127
+ existingKeys?: { static?: NoiseKeyPair; signing?: SigningKeyPair }
128
+ ) {
129
+ super();
130
+ this.config = { ...DEFAULT_CONFIG, ...config };
131
+
132
+ // Initialize with provided keys, persisted keys, or generate new ones
133
+ if (existingKeys?.static && existingKeys?.signing) {
134
+ this.staticKeyPair = existingKeys.static;
135
+ this.signingKeyPair = existingKeys.signing;
136
+ } else {
137
+ const loaded = loadOrCreateKeys();
138
+ this.staticKeyPair = loaded.static;
139
+ this.signingKeyPair = loaded.signing;
140
+ }
141
+
142
+ // Derive peer ID from static public key
143
+ this.myPeerID = PeerID.fromPublicKey(this.staticKeyPair.publicKey);
144
+ this.fingerprint = fingerprint(this.staticKeyPair.publicKey);
145
+
146
+ console.log('[SessionManager] Identity:');
147
+ console.log(' PeerID:', this.myPeerID.toHex());
148
+ console.log(' Noise pubkey:', Buffer.from(this.staticKeyPair.publicKey).toString('hex'));
149
+ console.log(' Signing pubkey:', Buffer.from(this.signingKeyPair.publicKey).toString('hex'));
150
+ }
151
+
152
+ /**
153
+ * Check if we have an established session with a peer
154
+ */
155
+ hasSession(peerID: PeerID): boolean {
156
+ return this.sessions.has(peerID.toHex());
157
+ }
158
+
159
+ /**
160
+ * Check if we're in the middle of a handshake
161
+ */
162
+ hasHandshakeInProgress(peerID: PeerID): boolean {
163
+ return this.pendingHandshakes.has(peerID.toHex());
164
+ }
165
+
166
+ /**
167
+ * Get handshake state for a peer
168
+ */
169
+ getHandshakeState(peerID: PeerID): HandshakeState {
170
+ if (this.sessions.has(peerID.toHex())) {
171
+ return HandshakeState.ESTABLISHED;
172
+ }
173
+ if (this.pendingHandshakes.has(peerID.toHex())) {
174
+ return HandshakeState.HANDSHAKING;
175
+ }
176
+ return HandshakeState.NONE;
177
+ }
178
+
179
+ /**
180
+ * Get established session
181
+ */
182
+ getSession(peerID: PeerID): NoiseSession | undefined {
183
+ return this.sessions.get(peerID.toHex())?.session;
184
+ }
185
+
186
+ /**
187
+ * Initiate a handshake with a peer
188
+ */
189
+ initiateHandshake(peerID: PeerID): Uint8Array {
190
+ const key = peerID.toHex();
191
+
192
+ // Check if already have session or handshake in progress
193
+ if (this.sessions.has(key)) {
194
+ throw new Error('Session already established');
195
+ }
196
+ if (this.pendingHandshakes.has(key)) {
197
+ throw new Error('Handshake already in progress');
198
+ }
199
+
200
+ // Create handshake state
201
+ const state = new NoiseHandshake('initiator', this.staticKeyPair);
202
+
203
+ // Write first message
204
+ const message = state.writeMessage();
205
+
206
+ // Store pending handshake
207
+ const pending: PendingHandshake = {
208
+ state,
209
+ role: 'initiator',
210
+ startedAt: Date.now(),
211
+ };
212
+
213
+ // Set timeout
214
+ pending.timeoutHandle = setTimeout(() => {
215
+ this.handleHandshakeTimeout(peerID);
216
+ }, this.config.handshakeTimeoutMs);
217
+
218
+ this.pendingHandshakes.set(key, pending);
219
+
220
+ return message;
221
+ }
222
+
223
+ /**
224
+ * Handle incoming handshake message
225
+ */
226
+ handleHandshakeMessage(peerID: PeerID, message: Uint8Array): Uint8Array | null {
227
+ const key = peerID.toHex();
228
+
229
+ let pending = this.pendingHandshakes.get(key);
230
+
231
+ if (!pending) {
232
+ // No handshake in progress - we're the responder
233
+ const state = new NoiseHandshake('responder', this.staticKeyPair);
234
+ pending = {
235
+ state,
236
+ role: 'responder',
237
+ startedAt: Date.now(),
238
+ };
239
+
240
+ // Set timeout
241
+ pending.timeoutHandle = setTimeout(() => {
242
+ this.handleHandshakeTimeout(peerID);
243
+ }, this.config.handshakeTimeoutMs);
244
+
245
+ this.pendingHandshakes.set(key, pending);
246
+ }
247
+
248
+ try {
249
+ // Process incoming message
250
+ pending.state.readMessage(message);
251
+
252
+ // Check if handshake is complete
253
+ if (pending.state.isComplete()) {
254
+ this.completeHandshake(peerID, pending);
255
+ return null;
256
+ }
257
+
258
+ // Write response
259
+ const response = pending.state.writeMessage();
260
+
261
+ // Check again after writing
262
+ if (pending.state.isComplete()) {
263
+ this.completeHandshake(peerID, pending);
264
+ }
265
+
266
+ return response;
267
+ } catch (error) {
268
+ // Handshake failed
269
+ this.handleHandshakeFailure(peerID, error as Error);
270
+ return null;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Complete a successful handshake
276
+ */
277
+ private completeHandshake(peerID: PeerID, pending: PendingHandshake): void {
278
+ const key = peerID.toHex();
279
+
280
+ // Clear timeout
281
+ if (pending.timeoutHandle) {
282
+ clearTimeout(pending.timeoutHandle);
283
+ }
284
+
285
+ // Get remote public key
286
+ const remotePublicKey = pending.state.getRemoteStaticPublicKey();
287
+ if (!remotePublicKey) {
288
+ this.handleHandshakeFailure(peerID, new Error('No remote public key'));
289
+ return;
290
+ }
291
+
292
+ // Create session
293
+ const { send, receive, hash } = pending.state.split();
294
+ const session = new NoiseSession(send, receive, hash, remotePublicKey);
295
+
296
+ // Store session
297
+ this.sessions.set(key, {
298
+ session,
299
+ establishedAt: Date.now(),
300
+ remotePublicKey,
301
+ });
302
+
303
+ // Clean up pending
304
+ this.pendingHandshakes.delete(key);
305
+
306
+ // Update peer info
307
+ this.updatePeerInfo(peerID, { noisePublicKey: remotePublicKey });
308
+
309
+ // Emit event
310
+ this.emit('session:established', peerID, session);
311
+
312
+ // Send any queued messages
313
+ this.flushPendingMessages(peerID);
314
+ }
315
+
316
+ /**
317
+ * Handle handshake timeout
318
+ */
319
+ private handleHandshakeTimeout(peerID: PeerID): void {
320
+ this.handleHandshakeFailure(peerID, new Error('Handshake timeout'));
321
+ }
322
+
323
+ /**
324
+ * Handle handshake failure
325
+ */
326
+ private handleHandshakeFailure(peerID: PeerID, error: Error): void {
327
+ const key = peerID.toHex();
328
+ const pending = this.pendingHandshakes.get(key);
329
+
330
+ if (pending?.timeoutHandle) {
331
+ clearTimeout(pending.timeoutHandle);
332
+ }
333
+
334
+ this.pendingHandshakes.delete(key);
335
+ this.pendingMessages.delete(key);
336
+
337
+ this.emit('session:failed', peerID, error);
338
+ }
339
+
340
+ /**
341
+ * Encrypt data for a peer
342
+ */
343
+ encrypt(peerID: PeerID, plaintext: Uint8Array): Uint8Array {
344
+ const session = this.sessions.get(peerID.toHex())?.session;
345
+ if (!session) {
346
+ throw new Error('No session with peer');
347
+ }
348
+ return session.encrypt(plaintext);
349
+ }
350
+
351
+ /**
352
+ * Decrypt data from a peer
353
+ */
354
+ decrypt(peerID: PeerID, ciphertext: Uint8Array): Uint8Array {
355
+ const session = this.sessions.get(peerID.toHex())?.session;
356
+ if (!session) {
357
+ throw new Error('No session with peer');
358
+ }
359
+ return session.decrypt(ciphertext);
360
+ }
361
+
362
+ /**
363
+ * Queue a message for after handshake completion
364
+ */
365
+ queueMessage(peerID: PeerID, data: Uint8Array): void {
366
+ const key = peerID.toHex();
367
+ const queue = this.pendingMessages.get(key) ?? [];
368
+ queue.push({ data, timestamp: Date.now() });
369
+ this.pendingMessages.set(key, queue);
370
+ }
371
+
372
+ /**
373
+ * Flush pending messages for a peer
374
+ */
375
+ private flushPendingMessages(peerID: PeerID): void {
376
+ const key = peerID.toHex();
377
+ const queue = this.pendingMessages.get(key);
378
+ if (!queue || queue.length === 0) return;
379
+
380
+ this.pendingMessages.delete(key);
381
+
382
+ for (const { data } of queue) {
383
+ try {
384
+ const encrypted = this.encrypt(peerID, data);
385
+ this.emit('handshake:message', peerID, encrypted);
386
+ } catch (error) {
387
+ console.error('Failed to encrypt queued message:', error);
388
+ }
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Close session with a peer
394
+ */
395
+ closeSession(peerID: PeerID): void {
396
+ const key = peerID.toHex();
397
+
398
+ const established = this.sessions.get(key);
399
+ if (established) {
400
+ established.session.clear();
401
+ this.sessions.delete(key);
402
+ }
403
+
404
+ const pending = this.pendingHandshakes.get(key);
405
+ if (pending?.timeoutHandle) {
406
+ clearTimeout(pending.timeoutHandle);
407
+ }
408
+ this.pendingHandshakes.delete(key);
409
+
410
+ this.pendingMessages.delete(key);
411
+
412
+ this.emit('session:closed', peerID);
413
+ }
414
+
415
+ /**
416
+ * Update peer info
417
+ */
418
+ updatePeerInfo(peerID: PeerID, updates: Partial<PeerInfo>): void {
419
+ const key = peerID.toHex();
420
+ const existing = this.peers.get(key) ?? {
421
+ peerID,
422
+ nickname: 'anon',
423
+ isConnected: false,
424
+ isVerified: false,
425
+ lastSeen: new Date(),
426
+ handshakeState: HandshakeState.NONE,
427
+ };
428
+
429
+ this.peers.set(key, { ...existing, ...updates });
430
+ }
431
+
432
+ /**
433
+ * Get peer info
434
+ */
435
+ getPeerInfo(peerID: PeerID): PeerInfo | undefined {
436
+ return this.peers.get(peerID.toHex());
437
+ }
438
+
439
+ /**
440
+ * Get all known peers
441
+ */
442
+ getAllPeers(): PeerInfo[] {
443
+ return Array.from(this.peers.values());
444
+ }
445
+
446
+ /**
447
+ * Get all peers with established sessions
448
+ */
449
+ getConnectedPeers(): PeerInfo[] {
450
+ const connectedKeys = new Set(this.sessions.keys());
451
+ return Array.from(this.peers.values()).filter((p) => connectedKeys.has(p.peerID.toHex()));
452
+ }
453
+
454
+ /**
455
+ * Clean up
456
+ */
457
+ destroy(): void {
458
+ // Clear all sessions
459
+ for (const [, established] of this.sessions) {
460
+ established.session.clear();
461
+ }
462
+ this.sessions.clear();
463
+
464
+ // Clear pending handshakes
465
+ for (const [, pending] of this.pendingHandshakes) {
466
+ if (pending.timeoutHandle) {
467
+ clearTimeout(pending.timeoutHandle);
468
+ }
469
+ }
470
+ this.pendingHandshakes.clear();
471
+
472
+ this.pendingMessages.clear();
473
+ this.peers.clear();
474
+ this.removeAllListeners();
475
+ }
476
+ }