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,345 @@
1
+ /**
2
+ * Binary Protocol Encoder/Decoder
3
+ * From: bitchat/Protocols/BinaryProtocol.swift
4
+ *
5
+ * Wire format:
6
+ * Header (14 bytes for v1, 16 bytes for v2):
7
+ * +--------+------+-----+-----------+-------+------------------+
8
+ * |Version | Type | TTL | Timestamp | Flags | PayloadLength |
9
+ * |1 byte |1 byte|1byte| 8 bytes | 1 byte| 2 or 4 bytes |
10
+ * +--------+------+-----+-----------+-------+------------------+
11
+ *
12
+ * Variable sections:
13
+ * +----------+-------------+---------+------------+
14
+ * | SenderID | RecipientID | Payload | Signature |
15
+ * | 8 bytes | 8 bytes* | Variable| 64 bytes* |
16
+ * +----------+-------------+---------+------------+
17
+ */
18
+
19
+ import {
20
+ MAX_FRAMED_FILE_BYTES,
21
+ PADDING_BLOCK_SIZES,
22
+ RECIPIENT_ID_SIZE,
23
+ SENDER_ID_SIZE,
24
+ SIGNATURE_SIZE,
25
+ V1_HEADER_SIZE,
26
+ V2_HEADER_SIZE,
27
+ } from './constants.js';
28
+ import { type BitchatPacket, type MessageType, PacketFlags, PeerID } from './types.js';
29
+
30
+ /**
31
+ * Calculate optimal padding size for traffic analysis resistance
32
+ */
33
+ function optimalBlockSize(dataLength: number): number {
34
+ for (const size of PADDING_BLOCK_SIZES) {
35
+ if (dataLength <= size - 1) return size;
36
+ }
37
+ return Math.ceil((dataLength + 1) / 256) * 256;
38
+ }
39
+
40
+ /**
41
+ * PKCS#7 style padding
42
+ */
43
+ function pad(data: Uint8Array, targetSize: number): Uint8Array {
44
+ const paddingLength = targetSize - data.length;
45
+ if (paddingLength <= 0) return data;
46
+
47
+ const padded = new Uint8Array(targetSize);
48
+ padded.set(data);
49
+ padded.fill(paddingLength, data.length);
50
+ return padded;
51
+ }
52
+
53
+ /**
54
+ * Remove PKCS#7 padding
55
+ */
56
+ function unpad(data: Uint8Array): Uint8Array {
57
+ if (data.length === 0) return data;
58
+
59
+ const paddingLength = data[data.length - 1];
60
+ if (paddingLength === 0 || paddingLength > data.length) return data;
61
+
62
+ // Verify padding is consistent
63
+ for (let i = data.length - paddingLength; i < data.length; i++) {
64
+ if (data[i] !== paddingLength) return data;
65
+ }
66
+
67
+ return data.subarray(0, data.length - paddingLength);
68
+ }
69
+
70
+ /**
71
+ * Encode a BitchatPacket to binary wire format
72
+ */
73
+ export function encode(packet: BitchatPacket, options: { padding?: boolean } = {}): Uint8Array {
74
+ const { padding = true } = options;
75
+ const version = packet.version;
76
+
77
+ if (version !== 1 && version !== 2) {
78
+ throw new Error(`Unsupported protocol version: ${version}`);
79
+ }
80
+
81
+ const headerSize = version === 2 ? V2_HEADER_SIZE : V1_HEADER_SIZE;
82
+
83
+ // Build flags
84
+ let flags = 0;
85
+ if (packet.recipientID && !packet.recipientID.isEmpty) flags |= PacketFlags.HAS_RECIPIENT;
86
+ if (packet.signature) flags |= PacketFlags.HAS_SIGNATURE;
87
+ if (packet.route && packet.route.length > 0 && version >= 2) flags |= PacketFlags.HAS_ROUTE;
88
+ if (packet.isRSR) flags |= PacketFlags.IS_RSR;
89
+ // TODO: compression flag
90
+
91
+ // Calculate sizes
92
+ const hasRecipient = (flags & PacketFlags.HAS_RECIPIENT) !== 0;
93
+ const hasSignature = (flags & PacketFlags.HAS_SIGNATURE) !== 0;
94
+ const hasRoute = (flags & PacketFlags.HAS_ROUTE) !== 0;
95
+
96
+ const routeLength = hasRoute ? 1 + (packet.route?.length ?? 0) * SENDER_ID_SIZE : 0;
97
+ const payloadSize = packet.payload.length;
98
+
99
+ // Validate sizes
100
+ if (version === 1 && payloadSize > 0xffff) {
101
+ throw new Error('Payload too large for v1 packet');
102
+ }
103
+ if (version === 2 && payloadSize > 0xffffffff) {
104
+ throw new Error('Payload too large for v2 packet');
105
+ }
106
+
107
+ // Calculate total size
108
+ const totalSize =
109
+ headerSize +
110
+ SENDER_ID_SIZE +
111
+ (hasRecipient ? RECIPIENT_ID_SIZE : 0) +
112
+ routeLength +
113
+ payloadSize +
114
+ (hasSignature ? SIGNATURE_SIZE : 0);
115
+
116
+ const buffer = new Uint8Array(totalSize);
117
+ const view = new DataView(buffer.buffer);
118
+ let offset = 0;
119
+
120
+ // Header
121
+ buffer[offset++] = version;
122
+ buffer[offset++] = packet.type;
123
+ buffer[offset++] = packet.ttl;
124
+
125
+ // Timestamp (8 bytes big-endian)
126
+ view.setBigUint64(offset, packet.timestamp, false);
127
+ offset += 8;
128
+
129
+ // Flags
130
+ buffer[offset++] = flags;
131
+
132
+ // Payload length
133
+ if (version === 2) {
134
+ view.setUint32(offset, payloadSize, false);
135
+ offset += 4;
136
+ } else {
137
+ view.setUint16(offset, payloadSize, false);
138
+ offset += 2;
139
+ }
140
+
141
+ // Sender ID
142
+ const senderBytes = packet.senderID.toBytes();
143
+ buffer.set(senderBytes, offset);
144
+ offset += SENDER_ID_SIZE;
145
+
146
+ // Recipient ID (optional)
147
+ if (hasRecipient && packet.recipientID) {
148
+ const recipientBytes = packet.recipientID.toBytes();
149
+ buffer.set(recipientBytes, offset);
150
+ offset += RECIPIENT_ID_SIZE;
151
+ }
152
+
153
+ // Route (optional, v2+)
154
+ if (hasRoute && packet.route) {
155
+ buffer[offset++] = packet.route.length;
156
+ for (const hop of packet.route) {
157
+ buffer.set(hop.toBytes(), offset);
158
+ offset += SENDER_ID_SIZE;
159
+ }
160
+ }
161
+
162
+ // Payload
163
+ buffer.set(packet.payload, offset);
164
+ offset += payloadSize;
165
+
166
+ // Signature (optional)
167
+ if (hasSignature && packet.signature) {
168
+ buffer.set(packet.signature.subarray(0, SIGNATURE_SIZE), offset);
169
+ offset += SIGNATURE_SIZE;
170
+ }
171
+
172
+ if (padding) {
173
+ const targetSize = optimalBlockSize(buffer.length);
174
+ return pad(buffer, targetSize);
175
+ }
176
+
177
+ return buffer;
178
+ }
179
+
180
+ /**
181
+ * Decode binary wire format to BitchatPacket
182
+ */
183
+ export function decode(data: Uint8Array): BitchatPacket | null {
184
+ // Try decode as-is first
185
+ let packet = decodeCore(data);
186
+ if (packet) return packet;
187
+
188
+ // Try after removing padding
189
+ const unpaddedData = unpad(data);
190
+ if (unpaddedData.length !== data.length) {
191
+ packet = decodeCore(unpaddedData);
192
+ }
193
+
194
+ return packet;
195
+ }
196
+
197
+ function decodeCore(data: Uint8Array): BitchatPacket | null {
198
+ if (data.length < V1_HEADER_SIZE + SENDER_ID_SIZE) {
199
+ return null;
200
+ }
201
+
202
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
203
+ let offset = 0;
204
+
205
+ const version = data[offset++];
206
+ if (version !== 1 && version !== 2) return null;
207
+
208
+ const headerSize = version === 2 ? V2_HEADER_SIZE : V1_HEADER_SIZE;
209
+ if (data.length < headerSize + SENDER_ID_SIZE) return null;
210
+
211
+ const type = data[offset++] as MessageType;
212
+ const ttl = data[offset++];
213
+
214
+ // Timestamp (8 bytes big-endian)
215
+ const timestamp = view.getBigUint64(offset, false);
216
+ offset += 8;
217
+
218
+ const flags = data[offset++];
219
+ const hasRecipient = (flags & PacketFlags.HAS_RECIPIENT) !== 0;
220
+ const hasSignature = (flags & PacketFlags.HAS_SIGNATURE) !== 0;
221
+ const isCompressed = (flags & PacketFlags.IS_COMPRESSED) !== 0;
222
+ const hasRoute = version >= 2 && (flags & PacketFlags.HAS_ROUTE) !== 0;
223
+ const isRSR = (flags & PacketFlags.IS_RSR) !== 0;
224
+
225
+ // Payload length
226
+ let payloadLength: number;
227
+ if (version === 2) {
228
+ payloadLength = view.getUint32(offset, false);
229
+ offset += 4;
230
+ } else {
231
+ payloadLength = view.getUint16(offset, false);
232
+ offset += 2;
233
+ }
234
+
235
+ // Validate
236
+ if (payloadLength < 0 || payloadLength > MAX_FRAMED_FILE_BYTES) return null;
237
+
238
+ // Sender ID
239
+ if (offset + SENDER_ID_SIZE > data.length) return null;
240
+ const senderID = new PeerID(data.subarray(offset, offset + SENDER_ID_SIZE));
241
+ offset += SENDER_ID_SIZE;
242
+
243
+ // Recipient ID
244
+ let recipientID: PeerID | undefined;
245
+ if (hasRecipient) {
246
+ if (offset + RECIPIENT_ID_SIZE > data.length) return null;
247
+ recipientID = new PeerID(data.subarray(offset, offset + RECIPIENT_ID_SIZE));
248
+ offset += RECIPIENT_ID_SIZE;
249
+ }
250
+
251
+ // Route
252
+ let route: PeerID[] | undefined;
253
+ if (hasRoute) {
254
+ if (offset >= data.length) return null;
255
+ const routeCount = data[offset++];
256
+ if (routeCount > 0) {
257
+ route = [];
258
+ for (let i = 0; i < routeCount; i++) {
259
+ if (offset + SENDER_ID_SIZE > data.length) return null;
260
+ route.push(new PeerID(data.subarray(offset, offset + SENDER_ID_SIZE)));
261
+ offset += SENDER_ID_SIZE;
262
+ }
263
+ }
264
+ }
265
+
266
+ // Payload
267
+ if (isCompressed) {
268
+ // TODO: Handle compression
269
+ // For now, just read the raw bytes
270
+ const lengthFieldSize = version === 2 ? 4 : 2;
271
+ if (payloadLength < lengthFieldSize) return null;
272
+ // Skip original size field
273
+ offset += lengthFieldSize;
274
+ const compressedSize = payloadLength - lengthFieldSize;
275
+ if (offset + compressedSize > data.length) return null;
276
+ // Return compressed data as-is for now
277
+ const payload = new Uint8Array(data.subarray(offset, offset + compressedSize));
278
+ offset += compressedSize;
279
+
280
+ // Signature
281
+ let signature: Uint8Array | undefined;
282
+ if (hasSignature) {
283
+ if (offset + SIGNATURE_SIZE > data.length) return null;
284
+ signature = new Uint8Array(data.subarray(offset, offset + SIGNATURE_SIZE));
285
+ }
286
+
287
+ return {
288
+ version,
289
+ type,
290
+ ttl,
291
+ timestamp,
292
+ senderID,
293
+ recipientID,
294
+ payload,
295
+ signature,
296
+ route,
297
+ isRSR,
298
+ };
299
+ }
300
+
301
+ if (offset + payloadLength > data.length) return null;
302
+ const payload = new Uint8Array(data.subarray(offset, offset + payloadLength));
303
+ offset += payloadLength;
304
+
305
+ // Signature
306
+ let signature: Uint8Array | undefined;
307
+ if (hasSignature) {
308
+ if (offset + SIGNATURE_SIZE > data.length) return null;
309
+ signature = new Uint8Array(data.subarray(offset, offset + SIGNATURE_SIZE));
310
+ }
311
+
312
+ return {
313
+ version,
314
+ type,
315
+ ttl,
316
+ timestamp,
317
+ senderID,
318
+ recipientID,
319
+ payload,
320
+ signature,
321
+ route,
322
+ isRSR,
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Create binary representation for signing (without signature, TTL=0)
328
+ */
329
+ export function encodeForSigning(packet: BitchatPacket): Uint8Array {
330
+ const unsigned: BitchatPacket = {
331
+ ...packet,
332
+ signature: undefined,
333
+ ttl: 0,
334
+ isRSR: false,
335
+ };
336
+ // Must use padding: true to match Swift's toBinaryDataForSigning()
337
+ return encode(unsigned, { padding: true });
338
+ }
339
+
340
+ /**
341
+ * Create message deduplication ID
342
+ */
343
+ export function makeDeduplicationID(packet: BitchatPacket): string {
344
+ return `${packet.senderID.toHex()}-${packet.timestamp}-${packet.type}`;
345
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Bitchat Protocol Constants
3
+ * From: bitchat/Protocols/BitchatProtocol.swift
4
+ * bitchat/Services/BLE/BLEService.swift
5
+ */
6
+
7
+ // BLE Service and Characteristic UUIDs
8
+ export const SERVICE_UUID = 'f47b5e2d-4a9e-4c5a-9b3f-8e1d2c3a4b5c'; // mainnet
9
+ export const SERVICE_UUID_TESTNET = 'f47b5e2d-4a9e-4c5a-9b3f-8e1d2c3a4b5a';
10
+ export const CHARACTERISTIC_UUID = 'a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d';
11
+
12
+ // Binary protocol sizes
13
+ export const SENDER_ID_SIZE = 8;
14
+ export const RECIPIENT_ID_SIZE = 8;
15
+ export const SIGNATURE_SIZE = 64;
16
+ export const V1_HEADER_SIZE = 14;
17
+ export const V2_HEADER_SIZE = 16;
18
+
19
+ // Protocol limits
20
+ export const DEFAULT_TTL = 7;
21
+ export const MAX_TTL = 7;
22
+ export const MAX_MESSAGE_LENGTH = 4096;
23
+ export const MAX_FRAMED_FILE_BYTES = 10 * 1024 * 1024; // 10MB
24
+ export const BLE_DEFAULT_FRAGMENT_SIZE = 182;
25
+ export const BLE_MAX_MTU = 512;
26
+
27
+ // Padding block sizes for traffic analysis resistance
28
+ export const PADDING_BLOCK_SIZES = [256, 512, 1024, 2048] as const;
29
+
30
+ // Timing constants (milliseconds)
31
+ export const ANNOUNCE_MIN_INTERVAL_MS = 5000;
32
+ export const MAINTENANCE_INTERVAL_MS = 30000;
33
+ export const CONNECT_RATE_LIMIT_INTERVAL_MS = 1000;
34
+ export const REACHABILITY_RETENTION_VERIFIED_MS = 300000; // 5 min
35
+ export const REACHABILITY_RETENTION_UNVERIFIED_MS = 60000; // 1 min
36
+
37
+ // Connection limits
38
+ export const MAX_CENTRAL_LINKS = 7;
39
+ export const MAX_IN_FLIGHT_ASSEMBLIES = 10;
40
+ export const HIGH_DEGREE_THRESHOLD = 5;
41
+
42
+ // Compression threshold
43
+ export const COMPRESSION_THRESHOLD = 256;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Protocol module exports
3
+ */
4
+
5
+ export { decode, encode, encodeForSigning, makeDeduplicationID } from './binary.js';
6
+ export * from './constants.js';
7
+ export {
8
+ type AnnouncementData,
9
+ decodeAnnouncement,
10
+ decodePrivateMessage,
11
+ encodeAnnouncement,
12
+ encodePrivateMessage,
13
+ type PrivateMessageData,
14
+ } from './packets.js';
15
+ export * from './types.js';
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Packet Payload Encoders/Decoders
3
+ * TLV (Type-Length-Value) format for announce and private message payloads
4
+ * From: bitchat/Protocols/Packets.swift
5
+ */
6
+
7
+ // TLV Types for Announcement
8
+ const AnnounceType = {
9
+ NICKNAME: 0x01,
10
+ NOISE_PUBLIC_KEY: 0x02,
11
+ SIGNING_PUBLIC_KEY: 0x03,
12
+ DIRECT_NEIGHBORS: 0x04,
13
+ } as const;
14
+
15
+ // TLV Types for Private Message
16
+ const PrivateMessageType = {
17
+ MESSAGE_ID: 0x00,
18
+ CONTENT: 0x01,
19
+ } as const;
20
+
21
+ /**
22
+ * Announcement payload data
23
+ */
24
+ export interface AnnouncementData {
25
+ nickname: string;
26
+ noisePublicKey?: Uint8Array;
27
+ signingPublicKey?: Uint8Array;
28
+ directNeighbors?: Uint8Array[];
29
+ }
30
+
31
+ /**
32
+ * Private message payload data
33
+ */
34
+ export interface PrivateMessageData {
35
+ messageID: string;
36
+ content: string;
37
+ }
38
+
39
+ /**
40
+ * Encode announcement payload as TLV
41
+ */
42
+ export function encodeAnnouncement(data: AnnouncementData): Uint8Array {
43
+ const parts: Uint8Array[] = [];
44
+
45
+ // Nickname TLV
46
+ const nicknameBytes = new TextEncoder().encode(data.nickname);
47
+ if (nicknameBytes.length > 255) {
48
+ throw new Error('Nickname too long');
49
+ }
50
+ parts.push(new Uint8Array([AnnounceType.NICKNAME, nicknameBytes.length]));
51
+ parts.push(nicknameBytes);
52
+
53
+ // Noise public key TLV
54
+ if (data.noisePublicKey && data.noisePublicKey.length > 0) {
55
+ parts.push(new Uint8Array([AnnounceType.NOISE_PUBLIC_KEY, data.noisePublicKey.length]));
56
+ parts.push(data.noisePublicKey);
57
+ }
58
+
59
+ // Signing public key TLV
60
+ if (data.signingPublicKey && data.signingPublicKey.length > 0) {
61
+ parts.push(new Uint8Array([AnnounceType.SIGNING_PUBLIC_KEY, data.signingPublicKey.length]));
62
+ parts.push(data.signingPublicKey);
63
+ }
64
+
65
+ // Direct neighbors TLV
66
+ if (data.directNeighbors && data.directNeighbors.length > 0) {
67
+ const neighborsData = new Uint8Array(data.directNeighbors.length * 8);
68
+ let offset = 0;
69
+ for (const neighbor of data.directNeighbors) {
70
+ neighborsData.set(neighbor.subarray(0, 8), offset);
71
+ offset += 8;
72
+ }
73
+ parts.push(new Uint8Array([AnnounceType.DIRECT_NEIGHBORS, neighborsData.length]));
74
+ parts.push(neighborsData);
75
+ }
76
+
77
+ // Concatenate all parts
78
+ const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
79
+ const result = new Uint8Array(totalLength);
80
+ let offset = 0;
81
+ for (const part of parts) {
82
+ result.set(part, offset);
83
+ offset += part.length;
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Decode announcement payload from TLV
91
+ */
92
+ export function decodeAnnouncement(data: Uint8Array): AnnouncementData | null {
93
+ let offset = 0;
94
+ let nickname: string | undefined;
95
+ let noisePublicKey: Uint8Array | undefined;
96
+ let signingPublicKey: Uint8Array | undefined;
97
+ let directNeighbors: Uint8Array[] | undefined;
98
+
99
+ while (offset + 2 <= data.length) {
100
+ const type = data[offset++];
101
+ const length = data[offset++];
102
+
103
+ if (offset + length > data.length) {
104
+ return null; // Truncated
105
+ }
106
+
107
+ const value = data.subarray(offset, offset + length);
108
+ offset += length;
109
+
110
+ switch (type) {
111
+ case AnnounceType.NICKNAME:
112
+ nickname = new TextDecoder().decode(value);
113
+ break;
114
+ case AnnounceType.NOISE_PUBLIC_KEY:
115
+ noisePublicKey = Uint8Array.from(value);
116
+ break;
117
+ case AnnounceType.SIGNING_PUBLIC_KEY:
118
+ signingPublicKey = Uint8Array.from(value);
119
+ break;
120
+ case AnnounceType.DIRECT_NEIGHBORS:
121
+ directNeighbors = [];
122
+ for (let i = 0; i + 8 <= value.length; i += 8) {
123
+ directNeighbors.push(Uint8Array.from(value.subarray(i, i + 8)));
124
+ }
125
+ break;
126
+ // Unknown types are ignored
127
+ }
128
+ }
129
+
130
+ if (!nickname) {
131
+ return null; // Nickname is required
132
+ }
133
+
134
+ return {
135
+ nickname,
136
+ noisePublicKey,
137
+ signingPublicKey,
138
+ directNeighbors,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Encode private message payload as TLV
144
+ */
145
+ export function encodePrivateMessage(data: PrivateMessageData): Uint8Array {
146
+ const parts: Uint8Array[] = [];
147
+
148
+ // Message ID TLV
149
+ const idBytes = new TextEncoder().encode(data.messageID);
150
+ parts.push(new Uint8Array([PrivateMessageType.MESSAGE_ID, idBytes.length]));
151
+ parts.push(idBytes);
152
+
153
+ // Content TLV
154
+ const contentBytes = new TextEncoder().encode(data.content);
155
+ if (contentBytes.length > 65535) {
156
+ throw new Error('Content too long');
157
+ }
158
+ // For content, use 2-byte length since it can be long
159
+ parts.push(
160
+ new Uint8Array([
161
+ PrivateMessageType.CONTENT,
162
+ (contentBytes.length >> 8) & 0xff,
163
+ contentBytes.length & 0xff,
164
+ ])
165
+ );
166
+ parts.push(contentBytes);
167
+
168
+ // Concatenate
169
+ const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
170
+ const result = new Uint8Array(totalLength);
171
+ let offset = 0;
172
+ for (const part of parts) {
173
+ result.set(part, offset);
174
+ offset += part.length;
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Decode private message payload from TLV
182
+ */
183
+ export function decodePrivateMessage(data: Uint8Array): PrivateMessageData | null {
184
+ let offset = 0;
185
+ let messageID: string | undefined;
186
+ let content: string | undefined;
187
+
188
+ while (offset + 2 <= data.length) {
189
+ const type = data[offset++];
190
+
191
+ // Content uses 2-byte length
192
+ let length: number;
193
+ if (type === PrivateMessageType.CONTENT) {
194
+ if (offset + 2 > data.length) return null;
195
+ length = (data[offset] << 8) | data[offset + 1];
196
+ offset += 2;
197
+ } else {
198
+ length = data[offset++];
199
+ }
200
+
201
+ if (offset + length > data.length) {
202
+ return null;
203
+ }
204
+
205
+ const value = data.subarray(offset, offset + length);
206
+ offset += length;
207
+
208
+ switch (type) {
209
+ case PrivateMessageType.MESSAGE_ID:
210
+ messageID = new TextDecoder().decode(value);
211
+ break;
212
+ case PrivateMessageType.CONTENT:
213
+ content = new TextDecoder().decode(value);
214
+ break;
215
+ }
216
+ }
217
+
218
+ if (!messageID || !content) {
219
+ return null;
220
+ }
221
+
222
+ return { messageID, content };
223
+ }