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.
- package/README.md +94 -23
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +143 -28
- package/dist/client.js.map +1 -1
- package/dist/debug.d.ts +39 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +89 -0
- package/dist/debug.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mesh/router.d.ts.map +1 -1
- package/dist/mesh/router.js +9 -1
- package/dist/mesh/router.js.map +1 -1
- package/dist/protocol/binary.d.ts +1 -1
- package/dist/protocol/binary.d.ts.map +1 -1
- package/dist/protocol/binary.js +2 -2
- package/dist/protocol/binary.js.map +1 -1
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +18 -1
- package/dist/session/manager.js.map +1 -1
- package/dist/transport/ble.d.ts +15 -0
- package/dist/transport/ble.d.ts.map +1 -1
- package/dist/transport/ble.js +63 -16
- package/dist/transport/ble.js.map +1 -1
- package/dist/ui/html.d.ts +5 -0
- package/dist/ui/html.d.ts.map +1 -0
- package/dist/ui/html.js +506 -0
- package/dist/ui/html.js.map +1 -0
- package/dist/ui/server.d.ts +5 -1
- package/dist/ui/server.d.ts.map +1 -1
- package/dist/ui/server.js +61 -255
- package/dist/ui/server.js.map +1 -1
- package/package.json +2 -1
- package/src/client.ts +159 -34
- package/src/debug.ts +119 -0
- package/src/index.ts +11 -0
- package/src/mesh/router.ts +11 -1
- package/src/protocol/binary.ts +2 -2
- package/src/session/manager.ts +19 -1
- package/src/transport/ble.ts +70 -16
- package/src/ui/html.ts +506 -0
- 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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
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,
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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';
|
package/src/mesh/router.ts
CHANGED
|
@@ -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
|
|
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);
|
package/src/protocol/binary.ts
CHANGED
|
@@ -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()
|
package/src/session/manager.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|