bitchat-node 0.1.1 → 0.1.2

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 (45) hide show
  1. package/README.md +94 -23
  2. package/dist/client.d.ts +1 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +143 -28
  5. package/dist/client.js.map +1 -1
  6. package/dist/debug.d.ts +39 -0
  7. package/dist/debug.d.ts.map +1 -0
  8. package/dist/debug.js +89 -0
  9. package/dist/debug.js.map +1 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/mesh/router.d.ts.map +1 -1
  15. package/dist/mesh/router.js +9 -1
  16. package/dist/mesh/router.js.map +1 -1
  17. package/dist/protocol/binary.d.ts +1 -1
  18. package/dist/protocol/binary.d.ts.map +1 -1
  19. package/dist/protocol/binary.js +2 -2
  20. package/dist/protocol/binary.js.map +1 -1
  21. package/dist/session/manager.d.ts.map +1 -1
  22. package/dist/session/manager.js +18 -1
  23. package/dist/session/manager.js.map +1 -1
  24. package/dist/transport/ble.d.ts +15 -0
  25. package/dist/transport/ble.d.ts.map +1 -1
  26. package/dist/transport/ble.js +63 -16
  27. package/dist/transport/ble.js.map +1 -1
  28. package/dist/ui/html.d.ts +5 -0
  29. package/dist/ui/html.d.ts.map +1 -0
  30. package/dist/ui/html.js +506 -0
  31. package/dist/ui/html.js.map +1 -0
  32. package/dist/ui/server.d.ts +5 -1
  33. package/dist/ui/server.d.ts.map +1 -1
  34. package/dist/ui/server.js +61 -255
  35. package/dist/ui/server.js.map +1 -1
  36. package/package.json +2 -1
  37. package/src/client.ts +159 -34
  38. package/src/debug.ts +119 -0
  39. package/src/index.ts +11 -0
  40. package/src/mesh/router.ts +11 -1
  41. package/src/protocol/binary.ts +2 -2
  42. package/src/session/manager.ts +19 -1
  43. package/src/transport/ble.ts +70 -16
  44. package/src/ui/html.ts +506 -0
  45. package/src/ui/server.ts +78 -258
package/src/client.ts CHANGED
@@ -100,8 +100,9 @@ export class BitchatClient extends EventEmitter {
100
100
  this.running = true;
101
101
  this.emit('ready');
102
102
 
103
- // Send initial announce
103
+ // Send initial announce and then periodic announces (like iOS does)
104
104
  setTimeout(() => this.sendAnnounce(), 1000);
105
+ setInterval(() => this.sendAnnounce(), 5000); // Every 5 seconds like iOS
105
106
  } catch (error) {
106
107
  this.emit('error', error as Error, 'start');
107
108
  throw error;
@@ -172,25 +173,47 @@ export class BitchatClient extends EventEmitter {
172
173
  */
173
174
  async sendPrivateMessage(content: string, toPeerID: PeerID): Promise<string> {
174
175
  const messageID = crypto.randomUUID();
176
+ const peerHex = toPeerID.toHex().slice(0, 8);
177
+
178
+ console.log(`[PM] Sending to ${peerHex}: "${content.slice(0, 30)}..."`);
175
179
 
176
180
  // Check if we have a session
177
181
  if (!this.sessions.hasSession(toPeerID)) {
178
- // Need to establish session first
179
- if (!this.sessions.hasHandshakeInProgress(toPeerID)) {
180
- // Initiate handshake
181
- const handshakeMsg = this.sessions.initiateHandshake(toPeerID);
182
- await this.sendHandshakePacket(toPeerID, handshakeMsg);
182
+ console.log(`[PM] No session with ${peerHex}`);
183
+
184
+ // Tie-breaker: only initiate if our peerID < target peerID
185
+ // This prevents both sides from initiating simultaneously
186
+ const weInitiate = this.peerID.toHex() < toPeerID.toHex();
187
+ console.log(`[PM] We initiate: ${weInitiate} (us: ${this.peerID.toHex().slice(0,8)} vs them: ${peerHex})`);
188
+
189
+ if (weInitiate) {
190
+ // Need to establish session first
191
+ if (!this.sessions.hasHandshakeInProgress(toPeerID)) {
192
+ console.log(`[PM] Initiating handshake with ${peerHex}`);
193
+ // Initiate handshake
194
+ const handshakeMsg = this.sessions.initiateHandshake(toPeerID);
195
+ console.log(`[PM] Handshake message: ${handshakeMsg.length} bytes`);
196
+ await this.sendHandshakePacket(toPeerID, handshakeMsg);
197
+ console.log(`[PM] Handshake packet sent`);
198
+ } else {
199
+ console.log(`[PM] Handshake already in progress with ${peerHex}`);
200
+ }
201
+ } else {
202
+ console.log(`[PM] Waiting for ${peerHex} to initiate handshake (they have lower ID)`);
183
203
  }
184
204
 
185
205
  // Queue message for after handshake
186
206
  const payload = this.buildPrivateMessagePayload(content, messageID);
187
207
  this.sessions.queueMessage(toPeerID, payload);
208
+ console.log(`[PM] Message queued for after handshake`);
188
209
 
189
210
  return messageID;
190
211
  }
191
212
 
213
+ console.log(`[PM] Session exists, encrypting and sending`);
192
214
  // Encrypt and send
193
215
  await this.sendEncryptedMessage(toPeerID, content, messageID);
216
+ console.log(`[PM] Encrypted message sent`);
194
217
  return messageID;
195
218
  }
196
219
 
@@ -239,13 +262,22 @@ export class BitchatClient extends EventEmitter {
239
262
 
240
263
  // Session events
241
264
  this.sessions.on('session:established', (peerID: PeerID, _session: NoiseSession) => {
265
+ const peerHex = peerID.toHex().slice(0, 8);
266
+ console.log(`[Session] ✓ Established with ${peerHex}`);
242
267
  const peer = this.sessions.getPeerInfo(peerID);
243
268
  if (peer) {
244
269
  this.emit('peer:updated', peer);
245
270
  }
246
271
  });
247
272
 
273
+ this.sessions.on('session:failed', (peerID: PeerID, error: Error) => {
274
+ const peerHex = peerID.toHex().slice(0, 8);
275
+ console.log(`[Session] ✗ Failed with ${peerHex}:`, error.message);
276
+ });
277
+
248
278
  this.sessions.on('handshake:message', async (peerID: PeerID, encrypted: Uint8Array) => {
279
+ const peerHex = peerID.toHex().slice(0, 8);
280
+ console.log(`[Session] Flushing queued message to ${peerHex}: ${encrypted.length} bytes`);
249
281
  // Send queued encrypted message
250
282
  const packet: BitchatPacket = {
251
283
  version: 1,
@@ -258,6 +290,7 @@ export class BitchatClient extends EventEmitter {
258
290
  isRSR: false,
259
291
  };
260
292
  await this.router.sendToPeer(packet, peerID);
293
+ console.log(`[Session] Queued message sent`);
261
294
  });
262
295
  }
263
296
 
@@ -269,6 +302,11 @@ export class BitchatClient extends EventEmitter {
269
302
  }
270
303
 
271
304
  private handlePacket(packet: BitchatPacket, fromLink: BLELink | null): void {
305
+ // Ignore our own packets (echoed back through mesh)
306
+ if (packet.senderID.equals(this.peerID)) {
307
+ return;
308
+ }
309
+
272
310
  switch (packet.type) {
273
311
  case MessageType.ANNOUNCE:
274
312
  this.handleAnnounce(packet, fromLink);
@@ -306,11 +344,13 @@ export class BitchatClient extends EventEmitter {
306
344
  // Verify signature if present
307
345
  if (packet.signature && announcement?.signingPublicKey) {
308
346
  const dataToSign = encodeForSigning(packet);
347
+ console.log('[Announce] Verifying', nickname, '- dataToSign first 60:', Buffer.from(dataToSign.slice(0, 60)).toString('hex'));
309
348
  const isValid = verify(dataToSign, packet.signature, announcement.signingPublicKey);
310
349
  if (!isValid) {
311
350
  console.warn('[Announce] Invalid signature from', nickname, peerID.toHex().slice(0, 8));
312
351
  return; // Reject invalid announces
313
352
  }
353
+ console.log('[Announce] Valid signature from', nickname);
314
354
  }
315
355
 
316
356
  // Check if this is a NEW peer or an existing one
@@ -329,7 +369,10 @@ export class BitchatClient extends EventEmitter {
329
369
 
330
370
  // Associate peer ID with link
331
371
  if (fromLink) {
372
+ console.log(`[Announce] Setting peer ${peerID.toHex().slice(0,8)} on link ${fromLink.id.slice(0,8)}`);
332
373
  fromLink.setPeerID(peerID);
374
+ } else {
375
+ console.log(`[Announce] No fromLink for peer ${peerID.toHex().slice(0,8)} - cannot associate!`);
333
376
  }
334
377
 
335
378
  const peer = this.sessions.getPeerInfo(peerID);
@@ -373,10 +416,16 @@ export class BitchatClient extends EventEmitter {
373
416
 
374
417
  private async handleHandshake(packet: BitchatPacket): Promise<void> {
375
418
  const peerID = packet.senderID;
419
+ const peerHex = peerID.toHex().slice(0, 8);
420
+ console.log(`[Handshake] Received from ${peerHex}: ${packet.payload.length} bytes`);
421
+
376
422
  const response = this.sessions.handleHandshakeMessage(peerID, packet.payload);
377
423
 
378
424
  if (response) {
425
+ console.log(`[Handshake] Sending response to ${peerHex}: ${response.length} bytes`);
379
426
  await this.sendHandshakePacket(peerID, response);
427
+ } else {
428
+ console.log(`[Handshake] No response needed (complete or error)`);
380
429
  }
381
430
  }
382
431
 
@@ -385,39 +434,76 @@ export class BitchatClient extends EventEmitter {
385
434
 
386
435
  try {
387
436
  const decrypted = this.sessions.decrypt(peerID, packet.payload);
388
-
389
- // First byte is payload type
390
- const payloadType = decrypted[0] as NoisePayloadType;
391
- const payloadData = decrypted.subarray(1);
392
-
393
- switch (payloadType) {
394
- case NoisePayloadType.PRIVATE_MESSAGE:
395
- this.handlePrivateMessage(peerID, payloadData, packet.timestamp);
396
- break;
397
-
398
- case NoisePayloadType.READ_RECEIPT:
399
- // TODO: Handle read receipt
400
- break;
401
-
402
- case NoisePayloadType.DELIVERED:
403
- // TODO: Handle delivery confirmation
404
- break;
405
-
406
- default:
407
- // Unknown payload type
408
- break;
437
+
438
+ console.log(`[Encrypted] Decrypted ${decrypted.length} bytes from ${peerID.toHex().slice(0, 8)}`);
439
+ console.log(`[Encrypted] First 20 bytes: ${Buffer.from(decrypted.slice(0, 20)).toString('hex')}`);
440
+
441
+ // Check payload type byte
442
+ const payloadType = decrypted[0];
443
+
444
+ if (payloadType === NoisePayloadType.PRIVATE_MESSAGE) {
445
+ // TLV format: [0x01] [messageID TLV: 0x00, len, data] [content TLV: 0x01, len, data]
446
+ const tlvData = decrypted.subarray(1);
447
+ const parsed = this.parseTLVPrivateMessage(tlvData);
448
+
449
+ if (parsed) {
450
+ console.log(`[Encrypted] Parsed TLV: msgID="${parsed.messageID.slice(0, 20)}...", content="${parsed.content.slice(0, 30)}..."`);
451
+ this.handlePrivateMessage(peerID, new TextEncoder().encode(parsed.content), packet.timestamp, parsed.messageID);
452
+ } else {
453
+ console.log(`[Encrypted] Failed to parse TLV, treating as raw`);
454
+ this.handlePrivateMessage(peerID, tlvData, packet.timestamp);
455
+ }
456
+ } else if (payloadType === NoisePayloadType.READ_RECEIPT) {
457
+ console.log(`[Encrypted] Read receipt from ${peerID.toHex().slice(0, 8)}`);
458
+ // TODO: Handle read receipt
459
+ } else if (payloadType === NoisePayloadType.DELIVERED) {
460
+ console.log(`[Encrypted] Delivery confirmation from ${peerID.toHex().slice(0, 8)}`);
461
+ // TODO: Handle delivery confirmation
462
+ } else {
463
+ console.log(`[Encrypted] Unknown payload type: 0x${payloadType.toString(16)}`);
409
464
  }
410
465
  } catch (error) {
411
466
  this.emit('error', error as Error, 'decrypt');
412
467
  }
413
468
  }
469
+
470
+ private parseTLVPrivateMessage(data: Uint8Array): { messageID: string; content: string } | null {
471
+ let offset = 0;
472
+ let messageID: string | undefined;
473
+ let content: string | undefined;
474
+
475
+ while (offset + 2 <= data.length) {
476
+ const tlvType = data[offset++];
477
+ const tlvLen = data[offset++];
478
+
479
+ if (offset + tlvLen > data.length) break;
480
+
481
+ const tlvValue = data.subarray(offset, offset + tlvLen);
482
+ offset += tlvLen;
483
+
484
+ if (tlvType === 0x00) {
485
+ // messageID
486
+ messageID = new TextDecoder().decode(tlvValue);
487
+ } else if (tlvType === 0x01) {
488
+ // content
489
+ content = new TextDecoder().decode(tlvValue);
490
+ }
491
+ }
492
+
493
+ if (messageID && content) {
494
+ return { messageID, content };
495
+ }
496
+ return null;
497
+ }
414
498
 
415
- private handlePrivateMessage(senderID: PeerID, payload: Uint8Array, timestamp: bigint): void {
499
+ private handlePrivateMessage(senderID: PeerID, payload: Uint8Array, timestamp: bigint, messageID?: string): void {
416
500
  const content = new TextDecoder().decode(payload);
501
+ const id = messageID ?? `${senderID.toHex()}-${timestamp}-private`;
502
+
417
503
  const peer = this.sessions.getPeerInfo(senderID);
418
504
 
419
505
  const message: ChatMessage = {
420
- id: `${senderID.toHex()}-${timestamp}-private`,
506
+ id,
421
507
  sender: senderID,
422
508
  senderNickname: peer?.nickname ?? 'anon',
423
509
  content,
@@ -426,6 +512,7 @@ export class BitchatClient extends EventEmitter {
426
512
  deliveryStatus: { type: 'delivered', to: this.peerID, at: new Date() },
427
513
  };
428
514
 
515
+ console.log(`[PM] Received from ${peer?.nickname ?? 'anon'}: "${content.slice(0, 30)}..."`);
429
516
  this.emit('message', message);
430
517
  }
431
518
 
@@ -453,6 +540,13 @@ export class BitchatClient extends EventEmitter {
453
540
  const signature = sign(dataToSign, this.sessions.signingKeyPair.secretKey);
454
541
  packet.signature = signature;
455
542
 
543
+ // Debug: verify our own signature
544
+ const selfVerify = verify(dataToSign, signature, this.sessions.signingKeyPair.publicKey);
545
+ console.log('[Announce] Self-verify:', selfVerify ? 'PASS' : 'FAIL');
546
+ console.log('[Announce] DataToSign length:', dataToSign.length);
547
+ console.log('[Announce] DataToSign first 60:', Buffer.from(dataToSign.slice(0, 60)).toString('hex'));
548
+ console.log('[Announce] Signature:', Buffer.from(signature).toString('hex').slice(0, 40) + '...');
549
+
456
550
  await this.router.sendPacket(packet);
457
551
  const encoded = encode(packet);
458
552
  await this.transport.broadcast(encoded);
@@ -487,11 +581,42 @@ export class BitchatClient extends EventEmitter {
487
581
  await this.router.sendToPeer(packet, toPeerID);
488
582
  }
489
583
 
490
- private buildPrivateMessagePayload(content: string, _messageID: string): Uint8Array {
491
- const contentBytes = new TextEncoder().encode(content);
492
- const payload = new Uint8Array(1 + contentBytes.length);
493
- payload[0] = NoisePayloadType.PRIVATE_MESSAGE;
494
- payload.set(contentBytes, 1);
584
+ private buildPrivateMessagePayload(content: string, messageID: string): Uint8Array {
585
+ // iOS TLV format:
586
+ // [0x01] - NoisePayloadType.privateMessage
587
+ // [0x00][len][messageID UTF8] - TLV for messageID
588
+ // [0x01][len][content UTF8] - TLV for content
589
+ const encoder = new TextEncoder();
590
+ const messageIDBytes = encoder.encode(messageID);
591
+ const contentBytes = encoder.encode(content);
592
+
593
+ // Validate lengths (must fit in 1 byte)
594
+ if (messageIDBytes.length > 255 || contentBytes.length > 255) {
595
+ throw new Error('Message ID or content too long for TLV encoding');
596
+ }
597
+
598
+ // Build TLV payload: 1 + (1+1+msgIDLen) + (1+1+contentLen)
599
+ const payload = new Uint8Array(1 + 2 + messageIDBytes.length + 2 + contentBytes.length);
600
+ let offset = 0;
601
+
602
+ // NoisePayloadType.privateMessage = 0x01
603
+ payload[offset++] = 0x01;
604
+
605
+ // TLV for messageID: type=0x00, length, data
606
+ payload[offset++] = 0x00;
607
+ payload[offset++] = messageIDBytes.length;
608
+ payload.set(messageIDBytes, offset);
609
+ offset += messageIDBytes.length;
610
+
611
+ // TLV for content: type=0x01, length, data
612
+ payload[offset++] = 0x01;
613
+ payload[offset++] = contentBytes.length;
614
+ payload.set(contentBytes, offset);
615
+
616
+ console.log(`[PM] Building TLV payload: msgID="${messageID.slice(0, 20)}...", content="${content.slice(0, 20)}..."`);
617
+ console.log(`[PM] Total payload: ${payload.length} bytes`);
618
+ console.log(`[PM] First 50 bytes: ${Buffer.from(payload.slice(0, 50)).toString('hex')}`);
619
+
495
620
  return payload;
496
621
  }
497
622
 
package/src/debug.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Global debug logger for Bitchat
3
+ * Emits events that can be forwarded to UI/webhooks
4
+ */
5
+
6
+ import { EventEmitter } from 'node:events';
7
+
8
+ export interface DebugEvent {
9
+ category: 'ble-rx' | 'ble-tx' | 'session' | 'error' | 'packet' | 'pm';
10
+ tag?: string;
11
+ text?: string;
12
+ hex?: string;
13
+ decoded?: string;
14
+ time: number;
15
+ }
16
+
17
+ export interface SessionState {
18
+ peerID: string;
19
+ established: boolean;
20
+ initiator: boolean;
21
+ txNonce?: number;
22
+ rxNonce?: number;
23
+ error?: string;
24
+ }
25
+
26
+ class DebugLogger extends EventEmitter {
27
+ private enabled = true;
28
+ private logToConsole = true;
29
+
30
+ enable(): void {
31
+ this.enabled = true;
32
+ }
33
+
34
+ disable(): void {
35
+ this.enabled = false;
36
+ }
37
+
38
+ setConsoleLogging(enabled: boolean): void {
39
+ this.logToConsole = enabled;
40
+ }
41
+
42
+ log(event: Omit<DebugEvent, 'time'>): void {
43
+ if (!this.enabled) return;
44
+
45
+ const fullEvent: DebugEvent = {
46
+ ...event,
47
+ time: Date.now(),
48
+ };
49
+
50
+ this.emit('debug', fullEvent);
51
+
52
+ if (this.logToConsole) {
53
+ const tag = event.tag || event.category.toUpperCase();
54
+ const parts = [`[${tag}]`];
55
+ if (event.text) parts.push(event.text);
56
+ if (event.hex) parts.push(`hex: ${event.hex.slice(0, 60)}${event.hex.length > 60 ? '...' : ''}`);
57
+ if (event.decoded) parts.push(`→ ${event.decoded}`);
58
+ console.log(parts.join(' '));
59
+ }
60
+ }
61
+
62
+ // Convenience methods
63
+ bleRx(data: Uint8Array, decoded?: string): void {
64
+ this.log({
65
+ category: 'ble-rx',
66
+ tag: 'BLE RX',
67
+ hex: Buffer.from(data).toString('hex'),
68
+ decoded,
69
+ });
70
+ }
71
+
72
+ bleTx(data: Uint8Array, decoded?: string): void {
73
+ this.log({
74
+ category: 'ble-tx',
75
+ tag: 'BLE TX',
76
+ hex: Buffer.from(data).toString('hex'),
77
+ decoded,
78
+ });
79
+ }
80
+
81
+ session(peerID: string, text: string): void {
82
+ this.log({
83
+ category: 'session',
84
+ tag: 'Session',
85
+ text: `[${peerID.slice(0, 8)}] ${text}`,
86
+ });
87
+ }
88
+
89
+ packet(direction: 'rx' | 'tx', type: string, info: string): void {
90
+ this.log({
91
+ category: 'packet',
92
+ tag: `PKT ${direction.toUpperCase()}`,
93
+ text: `${type}: ${info}`,
94
+ });
95
+ }
96
+
97
+ pm(direction: 'rx' | 'tx', peerID: string, text: string): void {
98
+ this.log({
99
+ category: 'pm',
100
+ tag: `PM ${direction.toUpperCase()}`,
101
+ text: `[${peerID.slice(0, 8)}] ${text}`,
102
+ });
103
+ }
104
+
105
+ error(context: string, error: Error | string): void {
106
+ this.log({
107
+ category: 'error',
108
+ tag: 'ERROR',
109
+ text: `${context}: ${error instanceof Error ? error.message : error}`,
110
+ });
111
+ }
112
+
113
+ updateSession(state: SessionState): void {
114
+ this.emit('session', state);
115
+ }
116
+ }
117
+
118
+ // Global singleton
119
+ export const debugLog = new DebugLogger();
package/src/index.ts CHANGED
@@ -93,3 +93,14 @@ export {
93
93
  type BLETransportConfig,
94
94
  type BLETransportEvents,
95
95
  } from './transport/index.js';
96
+
97
+ // UI Server
98
+ export {
99
+ startUIServer,
100
+ type UIServerConfig,
101
+ type DebugEvent,
102
+ type SessionState,
103
+ } from './ui/server.js';
104
+
105
+ // Debug
106
+ export { debugLog } from './debug.js';
@@ -231,15 +231,25 @@ export class MeshRouter extends EventEmitter {
231
231
  * Send to a specific peer (if we have a direct link)
232
232
  */
233
233
  async sendToPeer(packet: BitchatPacket, peerID: PeerID): Promise<boolean> {
234
+ const peerHex = peerID.toHex().slice(0, 8);
235
+
234
236
  // Find link for this peer
235
- const link = Array.from(this.links.values()).find((l) => l.peerID?.equals(peerID));
237
+ const allLinks = Array.from(this.links.values());
238
+ console.log(`[Router] sendToPeer ${peerHex}: ${allLinks.length} links available`);
239
+ allLinks.forEach((l, i) => {
240
+ console.log(`[Router] Link ${i}: id=${l.id.slice(0,8)}, peerID=${l.peerID?.toHex().slice(0,8) ?? 'none'}`);
241
+ });
242
+
243
+ const link = allLinks.find((l) => l.peerID?.equals(peerID));
236
244
 
237
245
  if (!link) {
246
+ console.log(`[Router] No direct link to ${peerHex}, broadcasting instead`);
238
247
  // No direct link - broadcast and hope for relay
239
248
  await this.sendPacket(packet);
240
249
  return false;
241
250
  }
242
251
 
252
+ console.log(`[Router] Found direct link to ${peerHex}, sending directly`);
243
253
  const { encode } = await import('../protocol/binary.js');
244
254
  const data = encode(packet);
245
255
  await link.send(data);
@@ -326,11 +326,11 @@ function decodeCore(data: Uint8Array): BitchatPacket | null {
326
326
  /**
327
327
  * Create binary representation for signing (without signature, TTL=0)
328
328
  */
329
- export function encodeForSigning(packet: BitchatPacket): Uint8Array {
329
+ export function encodeForSigning(packet: BitchatPacket, preserveTTL = false): Uint8Array {
330
330
  const unsigned: BitchatPacket = {
331
331
  ...packet,
332
332
  signature: undefined,
333
- ttl: 0,
333
+ ttl: preserveTTL ? packet.ttl : 0, // Some implementations preserve TTL for verification
334
334
  isRSR: false,
335
335
  };
336
336
  // Must use padding: true to match Swift's toBinaryDataForSigning()
@@ -16,6 +16,7 @@ import {
16
16
  } from '../crypto/noise.js';
17
17
  import { fingerprint, generateSigningKeyPair, type SigningKeyPair } from '../crypto/signing.js';
18
18
  import { HandshakeState, PeerID, type PeerInfo } from '../protocol/types.js';
19
+ import { debugLog } from '../debug.js';
19
20
 
20
21
  // Key persistence path
21
22
  const KEY_DIR = join(homedir(), '.bitchat-node');
@@ -290,8 +291,17 @@ export class SessionManager extends EventEmitter {
290
291
  }
291
292
 
292
293
  // Create session
293
- const { send, receive, hash } = pending.state.split();
294
+ // iOS uses useExtractedNonce: true (4-byte nonce prepended to ciphertext)
295
+ const { send, receive, hash } = pending.state.split(true);
294
296
  const session = new NoiseSession(send, receive, hash, remotePublicKey);
297
+ debugLog.session(peerID.toHex(), `Session established (${pending.role}, extracted nonce mode)`);
298
+ debugLog.updateSession({
299
+ peerID: peerID.toHex(),
300
+ established: true,
301
+ initiator: pending.role === 'initiator',
302
+ txNonce: 0,
303
+ rxNonce: 0,
304
+ });
295
305
 
296
306
  // Store session
297
307
  this.sessions.set(key, {
@@ -327,6 +337,14 @@ export class SessionManager extends EventEmitter {
327
337
  const key = peerID.toHex();
328
338
  const pending = this.pendingHandshakes.get(key);
329
339
 
340
+ debugLog.session(key, `Handshake failed: ${error.message}`);
341
+ debugLog.updateSession({
342
+ peerID: key,
343
+ established: false,
344
+ initiator: pending?.role === 'initiator',
345
+ error: error.message,
346
+ });
347
+
330
348
  if (pending?.timeoutHandle) {
331
349
  clearTimeout(pending.timeoutHandle);
332
350
  }