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
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * bitchat-node
3
+ * Node.js implementation of the Bitchat BLE mesh protocol
4
+ *
5
+ * Compatible with Jack Dorsey's Bitchat app.
6
+ * @see https://github.com/permissionlesstech/bitchat
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { BitchatClient } from 'bitchat-node';
11
+ *
12
+ * const client = new BitchatClient({ nickname: 'my-node' });
13
+ *
14
+ * client.on('message', (message) => {
15
+ * console.log(`${message.senderNickname}: ${message.content}`);
16
+ * });
17
+ *
18
+ * client.on('peer:connected', (peer) => {
19
+ * console.log(`Connected to ${peer.nickname}`);
20
+ * });
21
+ *
22
+ * await client.start();
23
+ * await client.sendPublicMessage('Hello mesh!');
24
+ * ```
25
+ */
26
+
27
+ // Main client
28
+ export { BitchatClient, type BitchatClientConfig, type BitchatClientEvents } from './client.js';
29
+ // Crypto
30
+ export {
31
+ // Noise protocol
32
+ CipherState,
33
+ fingerprint,
34
+ formatFingerprint,
35
+ generateKeyPair,
36
+ // Signing
37
+ generateSigningKeyPair,
38
+ HandshakeState as NoiseHandshakeState,
39
+ type NoiseKeyPair,
40
+ type NoiseRole,
41
+ NoiseSession,
42
+ type SigningKeyPair,
43
+ sha256,
44
+ sign,
45
+ verify,
46
+ } from './crypto/index.js';
47
+ // Mesh
48
+ export {
49
+ type DeduplicatorConfig,
50
+ type Link,
51
+ MeshRouter,
52
+ MessageDeduplicator,
53
+ type RouterConfig,
54
+ } from './mesh/index.js';
55
+ // Protocol
56
+ export {
57
+ type BitchatPacket,
58
+ CHARACTERISTIC_UUID,
59
+ type ChatMessage,
60
+ DEFAULT_TTL,
61
+ type DeliveryStatus,
62
+ decode,
63
+ // Binary encoding
64
+ encode,
65
+ encodeForSigning,
66
+ HandshakeState,
67
+ MAX_MESSAGE_LENGTH,
68
+ MAX_TTL,
69
+ // Types
70
+ MessageType,
71
+ makeDeduplicationID,
72
+ NoisePayloadType,
73
+ PacketFlags,
74
+ PeerID,
75
+ type PeerInfo,
76
+ type ReadReceipt,
77
+ // Constants
78
+ SERVICE_UUID,
79
+ SERVICE_UUID_TESTNET,
80
+ } from './protocol/index.js';
81
+
82
+ // Sessions
83
+ export {
84
+ SessionManager,
85
+ type SessionManagerConfig,
86
+ type SessionManagerEvents,
87
+ } from './session/index.js';
88
+
89
+ // Transport
90
+ export {
91
+ BLELink,
92
+ BLETransport,
93
+ type BLETransportConfig,
94
+ type BLETransportEvents,
95
+ } from './transport/index.js';
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Message Deduplicator
3
+ * Prevents processing the same message multiple times in the mesh
4
+ * Uses a simple bloom-filter-like approach with time-based expiry
5
+ */
6
+
7
+ export interface DeduplicatorConfig {
8
+ maxSize: number;
9
+ maxAgeMs: number;
10
+ }
11
+
12
+ const DEFAULT_CONFIG: DeduplicatorConfig = {
13
+ maxSize: 10000,
14
+ maxAgeMs: 5 * 60 * 1000, // 5 minutes
15
+ };
16
+
17
+ /**
18
+ * Efficient message deduplication for mesh networks
19
+ */
20
+ export class MessageDeduplicator {
21
+ private seen = new Map<string, number>();
22
+ private readonly config: DeduplicatorConfig;
23
+ private cleanupTimer?: ReturnType<typeof setInterval>;
24
+
25
+ constructor(config: Partial<DeduplicatorConfig> = {}) {
26
+ this.config = { ...DEFAULT_CONFIG, ...config };
27
+
28
+ // Periodic cleanup
29
+ this.cleanupTimer = setInterval(() => this.cleanup(), 60000);
30
+ }
31
+
32
+ /**
33
+ * Check if message was seen and mark it as seen
34
+ * Returns true if this is a NEW message (not seen before)
35
+ */
36
+ markSeen(id: string): boolean {
37
+ const now = Date.now();
38
+
39
+ // Check if already seen
40
+ const existingTimestamp = this.seen.get(id);
41
+ if (existingTimestamp !== undefined) {
42
+ // Already seen - check if expired
43
+ if (now - existingTimestamp > this.config.maxAgeMs) {
44
+ // Expired, treat as new
45
+ this.seen.set(id, now);
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+
51
+ // New message - add to seen set
52
+ this.seen.set(id, now);
53
+
54
+ // Trim if too large
55
+ if (this.seen.size > this.config.maxSize) {
56
+ this.trimOldest();
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Check if message was seen (without marking)
64
+ */
65
+ hasSeen(id: string): boolean {
66
+ const timestamp = this.seen.get(id);
67
+ if (timestamp === undefined) return false;
68
+
69
+ // Check expiry
70
+ if (Date.now() - timestamp > this.config.maxAgeMs) {
71
+ this.seen.delete(id);
72
+ return false;
73
+ }
74
+
75
+ return true;
76
+ }
77
+
78
+ /**
79
+ * Remove expired entries
80
+ */
81
+ private cleanup(): void {
82
+ const now = Date.now();
83
+ const maxAge = this.config.maxAgeMs;
84
+
85
+ for (const [id, timestamp] of this.seen) {
86
+ if (now - timestamp > maxAge) {
87
+ this.seen.delete(id);
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Trim oldest entries when size limit exceeded
94
+ */
95
+ private trimOldest(): void {
96
+ // Convert to array, sort by timestamp, keep newest half
97
+ const entries = Array.from(this.seen.entries()).sort((a, b) => a[1] - b[1]);
98
+
99
+ const toRemove = Math.floor(entries.length / 2);
100
+ for (let i = 0; i < toRemove; i++) {
101
+ this.seen.delete(entries[i][0]);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Clear all entries
107
+ */
108
+ reset(): void {
109
+ this.seen.clear();
110
+ }
111
+
112
+ /**
113
+ * Stop cleanup timer
114
+ */
115
+ destroy(): void {
116
+ if (this.cleanupTimer) {
117
+ clearInterval(this.cleanupTimer);
118
+ this.cleanupTimer = undefined;
119
+ }
120
+ this.seen.clear();
121
+ }
122
+
123
+ /**
124
+ * Get current size
125
+ */
126
+ get size(): number {
127
+ return this.seen.size;
128
+ }
129
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mesh module exports
3
+ */
4
+
5
+ export { type DeduplicatorConfig, MessageDeduplicator } from './deduplicator.js';
6
+ export { type Link, MeshRouter, type RouterConfig, type RouterEvents } from './router.js';
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Mesh Router
3
+ * Handles TTL-based message flooding and routing decisions
4
+ */
5
+
6
+ import { EventEmitter } from 'node:events';
7
+ import { makeDeduplicationID } from '../protocol/binary.js';
8
+ import { DEFAULT_TTL, MAX_TTL } from '../protocol/constants.js';
9
+ import { type BitchatPacket, MessageType, type PeerID } from '../protocol/types.js';
10
+ import { MessageDeduplicator } from './deduplicator.js';
11
+
12
+ export interface RouterConfig {
13
+ maxTTL: number;
14
+ defaultTTL: number;
15
+ probabilisticRelay: boolean;
16
+ relayProbability: number;
17
+ }
18
+
19
+ const DEFAULT_ROUTER_CONFIG: RouterConfig = {
20
+ maxTTL: MAX_TTL,
21
+ defaultTTL: DEFAULT_TTL,
22
+ probabilisticRelay: false,
23
+ relayProbability: 0.8,
24
+ };
25
+
26
+ export interface Link {
27
+ id: string;
28
+ send(data: Uint8Array): Promise<void>;
29
+ peerID?: PeerID;
30
+ }
31
+
32
+ export interface RouterEvents {
33
+ packet: (packet: BitchatPacket, fromLink: Link | null) => void;
34
+ forward: (packet: BitchatPacket, toLinks: Link[]) => void;
35
+ drop: (packet: BitchatPacket, reason: string) => void;
36
+ }
37
+
38
+ /**
39
+ * Mesh message router with TTL-based flooding
40
+ */
41
+ export class MeshRouter extends EventEmitter {
42
+ private readonly config: RouterConfig;
43
+ private readonly deduplicator: MessageDeduplicator;
44
+ private readonly links = new Map<string, Link>();
45
+ private myPeerID: PeerID | null = null;
46
+
47
+ // Track ingress link for each message to avoid echo
48
+ private ingressLinks = new Map<string, string>();
49
+
50
+ constructor(config: Partial<RouterConfig> = {}) {
51
+ super();
52
+ this.config = { ...DEFAULT_ROUTER_CONFIG, ...config };
53
+ this.deduplicator = new MessageDeduplicator();
54
+ }
55
+
56
+ /**
57
+ * Set our own peer ID (to avoid processing our own messages)
58
+ */
59
+ setMyPeerID(peerID: PeerID): void {
60
+ this.myPeerID = peerID;
61
+ }
62
+
63
+ /**
64
+ * Get our own peer ID
65
+ */
66
+ getMyPeerID(): PeerID | null {
67
+ return this.myPeerID;
68
+ }
69
+
70
+ /**
71
+ * Add a link (connection to another peer)
72
+ */
73
+ addLink(link: Link): void {
74
+ this.links.set(link.id, link);
75
+ }
76
+
77
+ /**
78
+ * Remove a link
79
+ */
80
+ removeLink(linkId: string): void {
81
+ this.links.delete(linkId);
82
+ // Clean up any ingress tracking
83
+ for (const [msgId, ingressLinkId] of this.ingressLinks) {
84
+ if (ingressLinkId === linkId) {
85
+ this.ingressLinks.delete(msgId);
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get all active links
92
+ */
93
+ getLinks(): Link[] {
94
+ return Array.from(this.links.values());
95
+ }
96
+
97
+ /**
98
+ * Get peer IDs of all connected links
99
+ */
100
+ getConnectedPeerIDs(): PeerID[] {
101
+ return Array.from(this.links.values())
102
+ .filter((link): link is Link & { peerID: PeerID } => link.peerID !== undefined)
103
+ .map((link) => link.peerID);
104
+ }
105
+
106
+ /**
107
+ * Handle an incoming packet
108
+ */
109
+ async handlePacket(packet: BitchatPacket, fromLink: Link | null): Promise<boolean> {
110
+ const dedupID = makeDeduplicationID(packet);
111
+
112
+ // Check deduplication
113
+ if (!this.deduplicator.markSeen(dedupID)) {
114
+ this.emit('drop', packet, 'duplicate');
115
+ return false;
116
+ }
117
+
118
+ // Track ingress link
119
+ if (fromLink) {
120
+ this.ingressLinks.set(dedupID, fromLink.id);
121
+ }
122
+
123
+ // Validate TTL
124
+ if (packet.ttl > this.config.maxTTL) {
125
+ this.emit('drop', packet, 'ttl_exceeded');
126
+ return false;
127
+ }
128
+
129
+ // Emit packet for processing
130
+ this.emit('packet', packet, fromLink);
131
+
132
+ // Decide whether to forward
133
+ if (this.shouldForward(packet)) {
134
+ await this.forward(packet, fromLink?.id);
135
+ }
136
+
137
+ return true;
138
+ }
139
+
140
+ /**
141
+ * Send a new packet (originated by us)
142
+ */
143
+ async sendPacket(packet: BitchatPacket): Promise<void> {
144
+ const dedupID = makeDeduplicationID(packet);
145
+
146
+ // Mark as seen to avoid processing our own relayed copy
147
+ this.deduplicator.markSeen(dedupID);
148
+
149
+ // Broadcast to all links
150
+ await this.broadcast(packet);
151
+ }
152
+
153
+ /**
154
+ * Check if packet should be forwarded
155
+ */
156
+ private shouldForward(packet: BitchatPacket): boolean {
157
+ // Don't forward if TTL exhausted
158
+ if (packet.ttl <= 0) return false;
159
+
160
+ // Don't forward certain message types
161
+ switch (packet.type) {
162
+ case MessageType.REQUEST_SYNC:
163
+ // Sync requests are local-only
164
+ return false;
165
+ default:
166
+ break;
167
+ }
168
+
169
+ // Probabilistic relay under high load
170
+ if (this.config.probabilisticRelay) {
171
+ if (Math.random() > this.config.relayProbability) {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ return true;
177
+ }
178
+
179
+ /**
180
+ * Forward a packet (decrement TTL, send to other links)
181
+ */
182
+ private async forward(packet: BitchatPacket, excludeLinkId?: string): Promise<void> {
183
+ // Decrement TTL
184
+ const forwardPacket: BitchatPacket = {
185
+ ...packet,
186
+ ttl: packet.ttl - 1,
187
+ };
188
+
189
+ // Get links to forward to (excluding ingress)
190
+ const dedupID = makeDeduplicationID(packet);
191
+ const ingressLinkId = excludeLinkId ?? this.ingressLinks.get(dedupID);
192
+
193
+ const targetLinks = Array.from(this.links.values()).filter((link) => link.id !== ingressLinkId);
194
+
195
+ if (targetLinks.length === 0) return;
196
+
197
+ this.emit('forward', forwardPacket, targetLinks);
198
+
199
+ // Send to all target links
200
+ await this.sendToLinks(forwardPacket, targetLinks);
201
+ }
202
+
203
+ /**
204
+ * Broadcast to all links
205
+ */
206
+ private async broadcast(packet: BitchatPacket): Promise<void> {
207
+ const targetLinks = Array.from(this.links.values());
208
+ await this.sendToLinks(packet, targetLinks);
209
+ }
210
+
211
+ /**
212
+ * Send packet to specific links
213
+ */
214
+ private async sendToLinks(packet: BitchatPacket, links: Link[]): Promise<void> {
215
+ const { encode } = await import('../protocol/binary.js');
216
+ const data = encode(packet);
217
+
218
+ const sendPromises = links.map(async (link) => {
219
+ try {
220
+ await link.send(data);
221
+ } catch (error) {
222
+ // Log error but don't fail the whole broadcast
223
+ console.error(`Failed to send to link ${link.id}:`, error);
224
+ }
225
+ });
226
+
227
+ await Promise.allSettled(sendPromises);
228
+ }
229
+
230
+ /**
231
+ * Send to a specific peer (if we have a direct link)
232
+ */
233
+ async sendToPeer(packet: BitchatPacket, peerID: PeerID): Promise<boolean> {
234
+ // Find link for this peer
235
+ const link = Array.from(this.links.values()).find((l) => l.peerID?.equals(peerID));
236
+
237
+ if (!link) {
238
+ // No direct link - broadcast and hope for relay
239
+ await this.sendPacket(packet);
240
+ return false;
241
+ }
242
+
243
+ const { encode } = await import('../protocol/binary.js');
244
+ const data = encode(packet);
245
+ await link.send(data);
246
+ return true;
247
+ }
248
+
249
+ /**
250
+ * Clean up
251
+ */
252
+ destroy(): void {
253
+ this.deduplicator.destroy();
254
+ this.links.clear();
255
+ this.ingressLinks.clear();
256
+ this.removeAllListeners();
257
+ }
258
+ }