@meshwhisper/sdk 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.
- package/README.md +138 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +19 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chaff/index.d.ts +91 -0
- package/dist/chaff/index.d.ts.map +1 -0
- package/dist/chaff/index.js +268 -0
- package/dist/chaff/index.js.map +1 -0
- package/dist/cluster/index.d.ts +159 -0
- package/dist/cluster/index.d.ts.map +1 -0
- package/dist/cluster/index.js +393 -0
- package/dist/cluster/index.js.map +1 -0
- package/dist/compliance/index.d.ts +129 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +315 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/crypto/index.d.ts +65 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +146 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/group/index.d.ts +155 -0
- package/dist/group/index.d.ts.map +1 -0
- package/dist/group/index.js +560 -0
- package/dist/group/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/namespace/index.d.ts +155 -0
- package/dist/namespace/index.d.ts.map +1 -0
- package/dist/namespace/index.js +278 -0
- package/dist/namespace/index.js.map +1 -0
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +19 -0
- package/dist/node/index.js.map +1 -0
- package/dist/packet/index.d.ts +63 -0
- package/dist/packet/index.d.ts.map +1 -0
- package/dist/packet/index.js +244 -0
- package/dist/packet/index.js.map +1 -0
- package/dist/permissions/index.d.ts +107 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +282 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/persistence/idb-storage.d.ts +27 -0
- package/dist/persistence/idb-storage.d.ts.map +1 -0
- package/dist/persistence/idb-storage.js +75 -0
- package/dist/persistence/idb-storage.js.map +1 -0
- package/dist/persistence/index.d.ts +4 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +3 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/node-storage.d.ts +33 -0
- package/dist/persistence/node-storage.d.ts.map +1 -0
- package/dist/persistence/node-storage.js +90 -0
- package/dist/persistence/node-storage.js.map +1 -0
- package/dist/persistence/serialization.d.ts +4 -0
- package/dist/persistence/serialization.d.ts.map +1 -0
- package/dist/persistence/serialization.js +49 -0
- package/dist/persistence/serialization.js.map +1 -0
- package/dist/persistence/types.d.ts +29 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +5 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/ratchet/index.d.ts +80 -0
- package/dist/ratchet/index.d.ts.map +1 -0
- package/dist/ratchet/index.js +259 -0
- package/dist/ratchet/index.js.map +1 -0
- package/dist/reciprocity/index.d.ts +109 -0
- package/dist/reciprocity/index.d.ts.map +1 -0
- package/dist/reciprocity/index.js +311 -0
- package/dist/reciprocity/index.js.map +1 -0
- package/dist/relay/index.d.ts +87 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/index.js +286 -0
- package/dist/relay/index.js.map +1 -0
- package/dist/routing/index.d.ts +136 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +478 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sdk/index.d.ts +322 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +1530 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sybil/index.d.ts +123 -0
- package/dist/sybil/index.d.ts.map +1 -0
- package/dist/sybil/index.js +491 -0
- package/dist/sybil/index.js.map +1 -0
- package/dist/transport/browser/index.d.ts +34 -0
- package/dist/transport/browser/index.d.ts.map +1 -0
- package/dist/transport/browser/index.js +176 -0
- package/dist/transport/browser/index.js.map +1 -0
- package/dist/transport/local/index.d.ts +57 -0
- package/dist/transport/local/index.d.ts.map +1 -0
- package/dist/transport/local/index.js +442 -0
- package/dist/transport/local/index.js.map +1 -0
- package/dist/transport/negotiator/index.d.ts +79 -0
- package/dist/transport/negotiator/index.d.ts.map +1 -0
- package/dist/transport/negotiator/index.js +289 -0
- package/dist/transport/negotiator/index.js.map +1 -0
- package/dist/transport/node/index.d.ts +56 -0
- package/dist/transport/node/index.d.ts.map +1 -0
- package/dist/transport/node/index.js +209 -0
- package/dist/transport/node/index.js.map +1 -0
- package/dist/transport/noop/index.d.ts +11 -0
- package/dist/transport/noop/index.d.ts.map +1 -0
- package/dist/transport/noop/index.js +20 -0
- package/dist/transport/noop/index.js.map +1 -0
- package/dist/transport/p2p/index.d.ts +109 -0
- package/dist/transport/p2p/index.d.ts.map +1 -0
- package/dist/transport/p2p/index.js +237 -0
- package/dist/transport/p2p/index.js.map +1 -0
- package/dist/transport/websocket/index.d.ts +89 -0
- package/dist/transport/websocket/index.d.ts.map +1 -0
- package/dist/transport/websocket/index.js +498 -0
- package/dist/transport/websocket/index.js.map +1 -0
- package/dist/transport/websocket/serialize.d.ts +5 -0
- package/dist/transport/websocket/serialize.d.ts.map +1 -0
- package/dist/transport/websocket/serialize.js +55 -0
- package/dist/transport/websocket/serialize.js.map +1 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/x3dh/index.d.ts +120 -0
- package/dist/x3dh/index.d.ts.map +1 -0
- package/dist/x3dh/index.js +290 -0
- package/dist/x3dh/index.js.map +1 -0
- package/package.json +59 -0
- package/src/browser/index.ts +19 -0
- package/src/chaff/index.ts +340 -0
- package/src/cluster/index.ts +482 -0
- package/src/compliance/index.ts +407 -0
- package/src/crypto/index.ts +193 -0
- package/src/group/index.ts +719 -0
- package/src/index.ts +87 -0
- package/src/lz4js.d.ts +58 -0
- package/src/namespace/index.ts +336 -0
- package/src/node/index.ts +19 -0
- package/src/packet/index.ts +326 -0
- package/src/permissions/index.ts +405 -0
- package/src/persistence/idb-storage.ts +83 -0
- package/src/persistence/index.ts +3 -0
- package/src/persistence/node-storage.ts +96 -0
- package/src/persistence/serialization.ts +75 -0
- package/src/persistence/types.ts +33 -0
- package/src/ratchet/index.ts +363 -0
- package/src/reciprocity/index.ts +371 -0
- package/src/relay/index.ts +382 -0
- package/src/routing/index.ts +577 -0
- package/src/sdk/index.ts +1994 -0
- package/src/sybil/index.ts +661 -0
- package/src/transport/browser/index.ts +201 -0
- package/src/transport/local/index.ts +540 -0
- package/src/transport/negotiator/index.ts +397 -0
- package/src/transport/node/index.ts +234 -0
- package/src/transport/noop/index.ts +22 -0
- package/src/transport/p2p/index.ts +345 -0
- package/src/transport/websocket/index.ts +660 -0
- package/src/transport/websocket/serialize.ts +68 -0
- package/src/types.ts +275 -0
- package/src/x3dh/index.ts +388 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — Browser Transport
|
|
3
|
+
// Connects the SDK to a MeshWhisper Node using the native
|
|
4
|
+
// browser WebSocket API. Drop-in replacement for NodeTransport
|
|
5
|
+
// when running in a browser or PWA context.
|
|
6
|
+
// ============================================================
|
|
7
|
+
import { serializePacket, deserializePacket, HEADER_SIZE, } from '../websocket/serialize.js';
|
|
8
|
+
// ---- Constants ----
|
|
9
|
+
/** Foundation-hosted relay nodes. Used when node config is "mesh". */
|
|
10
|
+
export const FOUNDATION_RELAY_NODES = [
|
|
11
|
+
'wss://relay.meshwhisper.io',
|
|
12
|
+
];
|
|
13
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
14
|
+
const RECONNECT_MAX_MS = 30_000;
|
|
15
|
+
// ---- BrowserTransport ----
|
|
16
|
+
/**
|
|
17
|
+
* Transport that connects to a MeshWhisper Node using the native browser
|
|
18
|
+
* WebSocket API. Functionally identical to NodeTransport but uses
|
|
19
|
+
* `globalThis.WebSocket` instead of the `ws` npm package, so it bundles
|
|
20
|
+
* cleanly for browsers and PWAs.
|
|
21
|
+
*/
|
|
22
|
+
export class BrowserTransport {
|
|
23
|
+
nodeUrl;
|
|
24
|
+
getDestHashes;
|
|
25
|
+
pushConfig;
|
|
26
|
+
type = 'internet';
|
|
27
|
+
ws = null;
|
|
28
|
+
receiveCallbacks = [];
|
|
29
|
+
running = false;
|
|
30
|
+
reconnectAttempt = 0;
|
|
31
|
+
reconnectTimer = null;
|
|
32
|
+
constructor(nodeUrl, getDestHashes, pushConfig) {
|
|
33
|
+
this.nodeUrl = nodeUrl;
|
|
34
|
+
this.getDestHashes = getDestHashes;
|
|
35
|
+
this.pushConfig = pushConfig;
|
|
36
|
+
}
|
|
37
|
+
// ---- Transport interface ----
|
|
38
|
+
async start() {
|
|
39
|
+
this.running = true;
|
|
40
|
+
await this.connect();
|
|
41
|
+
}
|
|
42
|
+
async stop() {
|
|
43
|
+
this.running = false;
|
|
44
|
+
if (this.reconnectTimer) {
|
|
45
|
+
clearTimeout(this.reconnectTimer);
|
|
46
|
+
this.reconnectTimer = null;
|
|
47
|
+
}
|
|
48
|
+
if (this.ws) {
|
|
49
|
+
this.ws.close(1000, 'shutdown');
|
|
50
|
+
this.ws = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async isAvailable() {
|
|
54
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
55
|
+
}
|
|
56
|
+
async send(packet, _destination) {
|
|
57
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
58
|
+
throw new Error('BrowserTransport: not connected to Node');
|
|
59
|
+
}
|
|
60
|
+
const binary = serializePacket(packet);
|
|
61
|
+
this.ws.send(binary);
|
|
62
|
+
}
|
|
63
|
+
onReceive(callback) {
|
|
64
|
+
this.receiveCallbacks.push(callback);
|
|
65
|
+
}
|
|
66
|
+
// ---- Connection management ----
|
|
67
|
+
resolveUrl() {
|
|
68
|
+
if (this.nodeUrl === 'mesh') {
|
|
69
|
+
return FOUNDATION_RELAY_NODES[0];
|
|
70
|
+
}
|
|
71
|
+
return this.nodeUrl;
|
|
72
|
+
}
|
|
73
|
+
connect() {
|
|
74
|
+
const url = this.resolveUrl();
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let resolved = false;
|
|
77
|
+
const ws = new WebSocket(url);
|
|
78
|
+
ws.binaryType = 'arraybuffer';
|
|
79
|
+
ws.addEventListener('open', () => {
|
|
80
|
+
this.ws = ws;
|
|
81
|
+
this.reconnectAttempt = 0;
|
|
82
|
+
resolved = true;
|
|
83
|
+
ws.send(JSON.stringify(this.buildHello()));
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
ws.addEventListener('message', (event) => {
|
|
87
|
+
this.handleMessage(event.data);
|
|
88
|
+
});
|
|
89
|
+
ws.addEventListener('close', () => {
|
|
90
|
+
this.ws = null;
|
|
91
|
+
if (this.running)
|
|
92
|
+
this.scheduleReconnect();
|
|
93
|
+
});
|
|
94
|
+
ws.addEventListener('error', () => {
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
resolved = true;
|
|
97
|
+
reject(new Error(`BrowserTransport: failed to connect to ${url}`));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
scheduleReconnect() {
|
|
103
|
+
if (!this.running)
|
|
104
|
+
return;
|
|
105
|
+
const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt), RECONNECT_MAX_MS);
|
|
106
|
+
this.reconnectAttempt++;
|
|
107
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
108
|
+
if (!this.running)
|
|
109
|
+
return;
|
|
110
|
+
try {
|
|
111
|
+
await this.connect();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
this.scheduleReconnect();
|
|
115
|
+
}
|
|
116
|
+
}, delay);
|
|
117
|
+
}
|
|
118
|
+
// ---- Message handling ----
|
|
119
|
+
handleMessage(data) {
|
|
120
|
+
if (data instanceof ArrayBuffer && data.byteLength >= HEADER_SIZE) {
|
|
121
|
+
try {
|
|
122
|
+
const packet = deserializePacket(new Uint8Array(data));
|
|
123
|
+
const source = this.resolveUrl();
|
|
124
|
+
for (const cb of this.receiveCallbacks) {
|
|
125
|
+
try {
|
|
126
|
+
cb(packet, source);
|
|
127
|
+
}
|
|
128
|
+
catch { /* swallow */ }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Malformed packet — discard
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (typeof data === 'string') {
|
|
137
|
+
try {
|
|
138
|
+
JSON.parse(data);
|
|
139
|
+
// Reserved for future Node→client control messages
|
|
140
|
+
}
|
|
141
|
+
catch { /* not JSON */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ---- Dest hash / push token refresh ----
|
|
145
|
+
refreshDestHashes() {
|
|
146
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
147
|
+
return;
|
|
148
|
+
this.ws.send(JSON.stringify(this.buildHello()));
|
|
149
|
+
}
|
|
150
|
+
setPushConfig(pushConfig) {
|
|
151
|
+
this.pushConfig = pushConfig;
|
|
152
|
+
this.refreshDestHashes();
|
|
153
|
+
}
|
|
154
|
+
// ---- Private helpers ----
|
|
155
|
+
buildHello() {
|
|
156
|
+
const msg = {
|
|
157
|
+
type: 'hello',
|
|
158
|
+
destHashes: this.getDestHashes(),
|
|
159
|
+
};
|
|
160
|
+
if (this.pushConfig) {
|
|
161
|
+
if (this.pushConfig.platform === 'webpush') {
|
|
162
|
+
msg['pushSubscription'] = JSON.stringify(this.pushConfig.subscription);
|
|
163
|
+
msg['pushPlatform'] = 'webpush';
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
msg['pushToken'] = this.pushConfig.token;
|
|
167
|
+
msg['pushPlatform'] = this.pushConfig.platform;
|
|
168
|
+
if ('topic' in this.pushConfig && this.pushConfig.topic) {
|
|
169
|
+
msg['pushTopic'] = this.pushConfig.topic;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return msg;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/transport/browser/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Transport, Packet } from '../../types.js';
|
|
2
|
+
export declare class LocalTransport implements Transport {
|
|
3
|
+
readonly type: "local_net";
|
|
4
|
+
private readonly deviceId;
|
|
5
|
+
private readonly deviceIdHex;
|
|
6
|
+
private readonly udpPort;
|
|
7
|
+
private readonly tcpPort;
|
|
8
|
+
private udpSocket;
|
|
9
|
+
private tcpServer;
|
|
10
|
+
private announceTimer;
|
|
11
|
+
private pruneTimer;
|
|
12
|
+
private readonly discoveredPeers;
|
|
13
|
+
private readonly connections;
|
|
14
|
+
private readonly pendingConnections;
|
|
15
|
+
private receiveCallback;
|
|
16
|
+
private running;
|
|
17
|
+
constructor(deviceId: Uint8Array, options?: {
|
|
18
|
+
udpPort?: number;
|
|
19
|
+
tcpPort?: number;
|
|
20
|
+
});
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
isAvailable(): Promise<boolean>;
|
|
24
|
+
send(packet: Packet, destination: string): Promise<void>;
|
|
25
|
+
onReceive(callback: (packet: Packet, source: string) => void): void;
|
|
26
|
+
startDiscovery(): Promise<void>;
|
|
27
|
+
/** Build and broadcast a MWSP announcement datagram. */
|
|
28
|
+
private broadcastAnnouncement;
|
|
29
|
+
/** Process an incoming UDP announcement. */
|
|
30
|
+
private handleAnnouncement;
|
|
31
|
+
/** Remove peers whose last announcement is older than PEER_TTL_MS. */
|
|
32
|
+
private pruneStalePeers;
|
|
33
|
+
startListener(port?: number): Promise<void>;
|
|
34
|
+
connectToPeer(address: string, port: number): Promise<void>;
|
|
35
|
+
/** Handle an incoming TCP connection from a remote peer. */
|
|
36
|
+
private handleIncomingConnection;
|
|
37
|
+
/**
|
|
38
|
+
* Attach length-prefixed framing to a TCP connection.
|
|
39
|
+
*
|
|
40
|
+
* The first message on every connection is a 16-byte device ID used to
|
|
41
|
+
* register the peer. All subsequent messages are serialized Packets.
|
|
42
|
+
*
|
|
43
|
+
* @param conn The peer connection wrapper (mutated in place).
|
|
44
|
+
* @param isInitiator True if we initiated the connection.
|
|
45
|
+
*/
|
|
46
|
+
private setupTcpFraming;
|
|
47
|
+
/** Return the list of peers discovered via UDP announcements. */
|
|
48
|
+
getDiscoveredPeers(): Array<{
|
|
49
|
+
id: string;
|
|
50
|
+
address: string;
|
|
51
|
+
port: number;
|
|
52
|
+
}>;
|
|
53
|
+
/** Return the IDs of peers with an active TCP connection. */
|
|
54
|
+
getConnectedPeers(): string[];
|
|
55
|
+
}
|
|
56
|
+
export default LocalTransport;
|
|
57
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transport/local/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAe,MAAM,gBAAgB,CAAC;AAgGrE,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAU;IAGrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAGjC,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,UAAU,CAA+C;IAGjE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;IACrE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,eAAe,CAA2D;IAClF,OAAO,CAAC,OAAO,CAAS;gBAGtB,QAAQ,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAe5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CrB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAoB/B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB9D,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAQ7D,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCrC,wDAAwD;IACxD,OAAO,CAAC,qBAAqB;IAmB7B,4CAA4C;IAC5C,OAAO,CAAC,kBAAkB;IAoC1B,sEAAsE;IACtE,OAAO,CAAC,eAAe;IAmBjB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB3C,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCjE,4DAA4D;IAC5D,OAAO,CAAC,wBAAwB;IAiBhC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IAwFvB,iEAAiE;IACjE,kBAAkB,IAAI,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAQ1E,6DAA6D;IAC7D,iBAAiB,IAAI,MAAM,EAAE;CAG9B;AAED,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — Local Network Transport (LAN)
|
|
3
|
+
// Bearer: local_net
|
|
4
|
+
//
|
|
5
|
+
// Uses UDP broadcast for peer discovery and TCP for reliable
|
|
6
|
+
// data transfer. Designed for device self-clustering on the
|
|
7
|
+
// same subnet (phone ↔ laptop in the same home).
|
|
8
|
+
// ============================================================
|
|
9
|
+
import * as dgram from 'node:dgram';
|
|
10
|
+
import * as net from 'node:net';
|
|
11
|
+
// --- Constants ---
|
|
12
|
+
const MAGIC = 0x4d575350; // "MWSP"
|
|
13
|
+
const DEFAULT_UDP_PORT = 19205;
|
|
14
|
+
const DEFAULT_TCP_PORT = 19206;
|
|
15
|
+
const ANNOUNCE_INTERVAL_MS = 5_000;
|
|
16
|
+
const PEER_TTL_MS = 15_000;
|
|
17
|
+
const DEVICE_ID_LENGTH = 16;
|
|
18
|
+
const ANNOUNCEMENT_SIZE = 4 + DEVICE_ID_LENGTH + 2; // magic + id + port
|
|
19
|
+
const LENGTH_PREFIX_SIZE = 4; // uint32 big-endian frame header
|
|
20
|
+
// --- Helpers ---
|
|
21
|
+
/** Encode a 16-byte device ID to a hex string. */
|
|
22
|
+
function deviceIdToHex(buf) {
|
|
23
|
+
return Buffer.from(buf).toString('hex');
|
|
24
|
+
}
|
|
25
|
+
/** Decode a hex string back to a 16-byte Uint8Array. */
|
|
26
|
+
function hexToDeviceId(hex) {
|
|
27
|
+
return new Uint8Array(Buffer.from(hex, 'hex'));
|
|
28
|
+
}
|
|
29
|
+
/** Serialize a Packet to a binary buffer. */
|
|
30
|
+
function serializePacket(packet) {
|
|
31
|
+
const headerSize = 1 + // version
|
|
32
|
+
1 + // flags
|
|
33
|
+
8 + // destHash
|
|
34
|
+
16 + // senderEphemeralId
|
|
35
|
+
1 + // ttl
|
|
36
|
+
4; // payloadLength (uint32)
|
|
37
|
+
const buf = Buffer.alloc(headerSize + packet.encryptedPayload.length);
|
|
38
|
+
let offset = 0;
|
|
39
|
+
buf.writeUInt8(packet.version, offset);
|
|
40
|
+
offset += 1;
|
|
41
|
+
buf.writeUInt8(packet.flags, offset);
|
|
42
|
+
offset += 1;
|
|
43
|
+
Buffer.from(packet.destHash).copy(buf, offset, 0, 8);
|
|
44
|
+
offset += 8;
|
|
45
|
+
Buffer.from(packet.senderEphemeralId).copy(buf, offset, 0, 16);
|
|
46
|
+
offset += 16;
|
|
47
|
+
buf.writeUInt8(packet.ttl, offset);
|
|
48
|
+
offset += 1;
|
|
49
|
+
buf.writeUInt32BE(packet.encryptedPayload.length, offset);
|
|
50
|
+
offset += 4;
|
|
51
|
+
Buffer.from(packet.encryptedPayload).copy(buf, offset);
|
|
52
|
+
return buf;
|
|
53
|
+
}
|
|
54
|
+
/** Deserialize a binary buffer back into a Packet. */
|
|
55
|
+
function deserializePacket(buf) {
|
|
56
|
+
let offset = 0;
|
|
57
|
+
const version = buf.readUInt8(offset);
|
|
58
|
+
offset += 1;
|
|
59
|
+
const flags = buf.readUInt8(offset);
|
|
60
|
+
offset += 1;
|
|
61
|
+
const destHash = new Uint8Array(buf.subarray(offset, offset + 8));
|
|
62
|
+
offset += 8;
|
|
63
|
+
const senderEphemeralId = new Uint8Array(buf.subarray(offset, offset + 16));
|
|
64
|
+
offset += 16;
|
|
65
|
+
const ttl = buf.readUInt8(offset);
|
|
66
|
+
offset += 1;
|
|
67
|
+
const payloadLength = buf.readUInt32BE(offset);
|
|
68
|
+
offset += 4;
|
|
69
|
+
const encryptedPayload = new Uint8Array(buf.subarray(offset, offset + payloadLength));
|
|
70
|
+
return { version, flags, destHash, senderEphemeralId, ttl, payloadLength, encryptedPayload };
|
|
71
|
+
}
|
|
72
|
+
// ============================================================
|
|
73
|
+
// LocalTransport
|
|
74
|
+
// ============================================================
|
|
75
|
+
export class LocalTransport {
|
|
76
|
+
type = 'local_net';
|
|
77
|
+
// --- Configuration ---
|
|
78
|
+
deviceId;
|
|
79
|
+
deviceIdHex;
|
|
80
|
+
udpPort;
|
|
81
|
+
tcpPort;
|
|
82
|
+
// --- Networking ---
|
|
83
|
+
udpSocket = null;
|
|
84
|
+
tcpServer = null;
|
|
85
|
+
announceTimer = null;
|
|
86
|
+
pruneTimer = null;
|
|
87
|
+
// --- State ---
|
|
88
|
+
discoveredPeers = new Map();
|
|
89
|
+
connections = new Map();
|
|
90
|
+
pendingConnections = new Set(); // addresses currently being connected to
|
|
91
|
+
receiveCallback = null;
|
|
92
|
+
running = false;
|
|
93
|
+
constructor(deviceId, options) {
|
|
94
|
+
if (deviceId.length !== DEVICE_ID_LENGTH) {
|
|
95
|
+
throw new Error(`deviceId must be ${DEVICE_ID_LENGTH} bytes, got ${deviceId.length}`);
|
|
96
|
+
}
|
|
97
|
+
this.deviceId = deviceId;
|
|
98
|
+
this.deviceIdHex = deviceIdToHex(deviceId);
|
|
99
|
+
this.udpPort = options?.udpPort ?? DEFAULT_UDP_PORT;
|
|
100
|
+
this.tcpPort = options?.tcpPort ?? DEFAULT_TCP_PORT;
|
|
101
|
+
}
|
|
102
|
+
// --------------------------------------------------------
|
|
103
|
+
// Transport interface — lifecycle
|
|
104
|
+
// --------------------------------------------------------
|
|
105
|
+
async start() {
|
|
106
|
+
if (this.running)
|
|
107
|
+
return;
|
|
108
|
+
this.running = true;
|
|
109
|
+
await Promise.all([
|
|
110
|
+
this.startDiscovery(),
|
|
111
|
+
this.startListener(this.tcpPort),
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
async stop() {
|
|
115
|
+
if (!this.running)
|
|
116
|
+
return;
|
|
117
|
+
this.running = false;
|
|
118
|
+
// Clear timers
|
|
119
|
+
if (this.announceTimer) {
|
|
120
|
+
clearInterval(this.announceTimer);
|
|
121
|
+
this.announceTimer = null;
|
|
122
|
+
}
|
|
123
|
+
if (this.pruneTimer) {
|
|
124
|
+
clearInterval(this.pruneTimer);
|
|
125
|
+
this.pruneTimer = null;
|
|
126
|
+
}
|
|
127
|
+
// Close all TCP peer connections
|
|
128
|
+
for (const [, conn] of this.connections) {
|
|
129
|
+
conn.socket.destroy();
|
|
130
|
+
}
|
|
131
|
+
this.connections.clear();
|
|
132
|
+
this.pendingConnections.clear();
|
|
133
|
+
// Close TCP server
|
|
134
|
+
await new Promise((resolve) => {
|
|
135
|
+
if (this.tcpServer) {
|
|
136
|
+
this.tcpServer.close(() => resolve());
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
this.tcpServer = null;
|
|
143
|
+
// Close UDP socket
|
|
144
|
+
await new Promise((resolve) => {
|
|
145
|
+
if (this.udpSocket) {
|
|
146
|
+
this.udpSocket.close(() => resolve());
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
resolve();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
this.udpSocket = null;
|
|
153
|
+
this.discoveredPeers.clear();
|
|
154
|
+
}
|
|
155
|
+
async isAvailable() {
|
|
156
|
+
// Local network is available if we can bind a UDP socket.
|
|
157
|
+
// In practice this checks whether the OS networking stack is usable.
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
const probe = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
160
|
+
probe.on('error', () => {
|
|
161
|
+
probe.close();
|
|
162
|
+
resolve(false);
|
|
163
|
+
});
|
|
164
|
+
probe.bind(0, () => {
|
|
165
|
+
probe.close();
|
|
166
|
+
resolve(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// --------------------------------------------------------
|
|
171
|
+
// Transport interface — messaging
|
|
172
|
+
// --------------------------------------------------------
|
|
173
|
+
async send(packet, destination) {
|
|
174
|
+
const conn = this.connections.get(destination);
|
|
175
|
+
if (!conn) {
|
|
176
|
+
throw new Error(`No active connection to peer ${destination}`);
|
|
177
|
+
}
|
|
178
|
+
const payload = serializePacket(packet);
|
|
179
|
+
const frame = Buffer.alloc(LENGTH_PREFIX_SIZE + payload.length);
|
|
180
|
+
frame.writeUInt32BE(payload.length, 0);
|
|
181
|
+
payload.copy(frame, LENGTH_PREFIX_SIZE);
|
|
182
|
+
await new Promise((resolve, reject) => {
|
|
183
|
+
conn.socket.write(frame, (err) => {
|
|
184
|
+
if (err)
|
|
185
|
+
reject(err);
|
|
186
|
+
else
|
|
187
|
+
resolve();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
onReceive(callback) {
|
|
192
|
+
this.receiveCallback = callback;
|
|
193
|
+
}
|
|
194
|
+
// --------------------------------------------------------
|
|
195
|
+
// UDP Discovery
|
|
196
|
+
// --------------------------------------------------------
|
|
197
|
+
async startDiscovery() {
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
this.udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
200
|
+
this.udpSocket.on('error', (err) => {
|
|
201
|
+
if (!this.running)
|
|
202
|
+
return;
|
|
203
|
+
// Non-fatal in steady state; during bind it rejects the promise.
|
|
204
|
+
reject(err);
|
|
205
|
+
});
|
|
206
|
+
this.udpSocket.on('message', (msg, rinfo) => {
|
|
207
|
+
this.handleAnnouncement(msg, rinfo.address);
|
|
208
|
+
});
|
|
209
|
+
this.udpSocket.bind(this.udpPort, () => {
|
|
210
|
+
this.udpSocket.setBroadcast(true);
|
|
211
|
+
// Send first announcement immediately, then on interval
|
|
212
|
+
this.broadcastAnnouncement();
|
|
213
|
+
this.announceTimer = setInterval(() => this.broadcastAnnouncement(), ANNOUNCE_INTERVAL_MS);
|
|
214
|
+
// Periodically prune stale peers
|
|
215
|
+
this.pruneTimer = setInterval(() => this.pruneStalePeers(), ANNOUNCE_INTERVAL_MS);
|
|
216
|
+
resolve();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Build and broadcast a MWSP announcement datagram. */
|
|
221
|
+
broadcastAnnouncement() {
|
|
222
|
+
if (!this.udpSocket)
|
|
223
|
+
return;
|
|
224
|
+
const buf = Buffer.alloc(ANNOUNCEMENT_SIZE);
|
|
225
|
+
let offset = 0;
|
|
226
|
+
buf.writeUInt32BE(MAGIC, offset);
|
|
227
|
+
offset += 4;
|
|
228
|
+
Buffer.from(this.deviceId).copy(buf, offset, 0, DEVICE_ID_LENGTH);
|
|
229
|
+
offset += DEVICE_ID_LENGTH;
|
|
230
|
+
buf.writeUInt16BE(this.tcpPort, offset);
|
|
231
|
+
this.udpSocket.send(buf, 0, buf.length, this.udpPort, '255.255.255.255', (err) => {
|
|
232
|
+
if (err && this.running) {
|
|
233
|
+
// Best-effort; swallow transient send errors.
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/** Process an incoming UDP announcement. */
|
|
238
|
+
handleAnnouncement(msg, senderAddress) {
|
|
239
|
+
if (msg.length < ANNOUNCEMENT_SIZE)
|
|
240
|
+
return;
|
|
241
|
+
const magic = msg.readUInt32BE(0);
|
|
242
|
+
if (magic !== MAGIC)
|
|
243
|
+
return;
|
|
244
|
+
const peerIdBytes = msg.subarray(4, 4 + DEVICE_ID_LENGTH);
|
|
245
|
+
const peerId = deviceIdToHex(peerIdBytes);
|
|
246
|
+
// Ignore our own announcements
|
|
247
|
+
if (peerId === this.deviceIdHex)
|
|
248
|
+
return;
|
|
249
|
+
const tcpPort = msg.readUInt16BE(4 + DEVICE_ID_LENGTH);
|
|
250
|
+
const existing = this.discoveredPeers.get(peerId);
|
|
251
|
+
if (existing) {
|
|
252
|
+
existing.lastSeen = Date.now();
|
|
253
|
+
existing.address = senderAddress;
|
|
254
|
+
existing.port = tcpPort;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.discoveredPeers.set(peerId, {
|
|
258
|
+
id: peerId,
|
|
259
|
+
address: senderAddress,
|
|
260
|
+
port: tcpPort,
|
|
261
|
+
lastSeen: Date.now(),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// Auto-connect if we don't already have a TCP connection
|
|
265
|
+
if (!this.connections.has(peerId) && !this.pendingConnections.has(peerId)) {
|
|
266
|
+
this.connectToPeer(senderAddress, tcpPort).catch(() => {
|
|
267
|
+
// Connection failed; will retry on next announcement.
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Remove peers whose last announcement is older than PEER_TTL_MS. */
|
|
272
|
+
pruneStalePeers() {
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
for (const [id, peer] of this.discoveredPeers) {
|
|
275
|
+
if (now - peer.lastSeen > PEER_TTL_MS) {
|
|
276
|
+
this.discoveredPeers.delete(id);
|
|
277
|
+
// Also tear down stale TCP connections
|
|
278
|
+
const conn = this.connections.get(id);
|
|
279
|
+
if (conn) {
|
|
280
|
+
conn.socket.destroy();
|
|
281
|
+
this.connections.delete(id);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// --------------------------------------------------------
|
|
287
|
+
// TCP Data Channel
|
|
288
|
+
// --------------------------------------------------------
|
|
289
|
+
async startListener(port) {
|
|
290
|
+
const listenPort = port ?? this.tcpPort;
|
|
291
|
+
await new Promise((resolve, reject) => {
|
|
292
|
+
this.tcpServer = net.createServer((socket) => {
|
|
293
|
+
this.handleIncomingConnection(socket);
|
|
294
|
+
});
|
|
295
|
+
this.tcpServer.on('error', (err) => {
|
|
296
|
+
reject(err);
|
|
297
|
+
});
|
|
298
|
+
this.tcpServer.listen(listenPort, () => {
|
|
299
|
+
resolve();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async connectToPeer(address, port) {
|
|
304
|
+
// Derive a temporary key until the peer identifies itself via handshake.
|
|
305
|
+
const addrKey = `${address}:${port}`;
|
|
306
|
+
this.pendingConnections.add(addrKey);
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const socket = net.createConnection({ host: address, port }, () => {
|
|
309
|
+
// Send our device ID so the remote side knows who connected
|
|
310
|
+
const idFrame = Buffer.alloc(LENGTH_PREFIX_SIZE + DEVICE_ID_LENGTH);
|
|
311
|
+
idFrame.writeUInt32BE(DEVICE_ID_LENGTH, 0);
|
|
312
|
+
Buffer.from(this.deviceId).copy(idFrame, LENGTH_PREFIX_SIZE);
|
|
313
|
+
socket.write(idFrame);
|
|
314
|
+
// We don't yet know the peer ID. We'll register the connection
|
|
315
|
+
// once we receive the peer's ID frame back.
|
|
316
|
+
const conn = {
|
|
317
|
+
peerId: '', // will be populated
|
|
318
|
+
socket,
|
|
319
|
+
recvBuffer: Buffer.alloc(0),
|
|
320
|
+
};
|
|
321
|
+
this.setupTcpFraming(conn, true);
|
|
322
|
+
this.pendingConnections.delete(addrKey);
|
|
323
|
+
resolve();
|
|
324
|
+
});
|
|
325
|
+
socket.on('error', (err) => {
|
|
326
|
+
this.pendingConnections.delete(addrKey);
|
|
327
|
+
reject(err);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/** Handle an incoming TCP connection from a remote peer. */
|
|
332
|
+
handleIncomingConnection(socket) {
|
|
333
|
+
const conn = {
|
|
334
|
+
peerId: '', // unknown until the peer sends its ID frame
|
|
335
|
+
socket,
|
|
336
|
+
recvBuffer: Buffer.alloc(0),
|
|
337
|
+
};
|
|
338
|
+
// The first framed message from the connecting side is the device ID.
|
|
339
|
+
this.setupTcpFraming(conn, false);
|
|
340
|
+
// Send our own ID back so the remote side can register us.
|
|
341
|
+
const idFrame = Buffer.alloc(LENGTH_PREFIX_SIZE + DEVICE_ID_LENGTH);
|
|
342
|
+
idFrame.writeUInt32BE(DEVICE_ID_LENGTH, 0);
|
|
343
|
+
Buffer.from(this.deviceId).copy(idFrame, LENGTH_PREFIX_SIZE);
|
|
344
|
+
socket.write(idFrame);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Attach length-prefixed framing to a TCP connection.
|
|
348
|
+
*
|
|
349
|
+
* The first message on every connection is a 16-byte device ID used to
|
|
350
|
+
* register the peer. All subsequent messages are serialized Packets.
|
|
351
|
+
*
|
|
352
|
+
* @param conn The peer connection wrapper (mutated in place).
|
|
353
|
+
* @param isInitiator True if we initiated the connection.
|
|
354
|
+
*/
|
|
355
|
+
setupTcpFraming(conn, isInitiator) {
|
|
356
|
+
let identified = false;
|
|
357
|
+
conn.socket.on('data', (chunk) => {
|
|
358
|
+
conn.recvBuffer = Buffer.concat([conn.recvBuffer, chunk]);
|
|
359
|
+
// Process as many complete frames as available
|
|
360
|
+
while (conn.recvBuffer.length >= LENGTH_PREFIX_SIZE) {
|
|
361
|
+
const frameLen = conn.recvBuffer.readUInt32BE(0);
|
|
362
|
+
// Guard against absurdly large frames (16 MiB limit)
|
|
363
|
+
if (frameLen > 16 * 1024 * 1024) {
|
|
364
|
+
conn.socket.destroy(new Error('Frame too large'));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (conn.recvBuffer.length < LENGTH_PREFIX_SIZE + frameLen) {
|
|
368
|
+
break; // wait for more data
|
|
369
|
+
}
|
|
370
|
+
const frameData = conn.recvBuffer.subarray(LENGTH_PREFIX_SIZE, LENGTH_PREFIX_SIZE + frameLen);
|
|
371
|
+
conn.recvBuffer = Buffer.from(conn.recvBuffer.subarray(LENGTH_PREFIX_SIZE + frameLen));
|
|
372
|
+
if (!identified) {
|
|
373
|
+
// First frame: device ID
|
|
374
|
+
if (frameData.length !== DEVICE_ID_LENGTH) {
|
|
375
|
+
conn.socket.destroy(new Error('Invalid identification frame'));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const peerId = deviceIdToHex(frameData);
|
|
379
|
+
// Don't connect to ourselves
|
|
380
|
+
if (peerId === this.deviceIdHex) {
|
|
381
|
+
conn.socket.destroy();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// If we already have a connection to this peer, keep only one.
|
|
385
|
+
// The tie-breaker: the side with the lexicographically smaller ID
|
|
386
|
+
// keeps its *initiated* connection.
|
|
387
|
+
const existingConn = this.connections.get(peerId);
|
|
388
|
+
if (existingConn) {
|
|
389
|
+
const weAreSmaller = this.deviceIdHex < peerId;
|
|
390
|
+
if (isInitiator === weAreSmaller) {
|
|
391
|
+
// We keep this connection; destroy the old one.
|
|
392
|
+
existingConn.socket.destroy();
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// We keep the existing connection; destroy this one.
|
|
396
|
+
conn.socket.destroy();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
conn.peerId = peerId;
|
|
401
|
+
this.connections.set(peerId, conn);
|
|
402
|
+
identified = true;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// Subsequent frames: Packets
|
|
406
|
+
try {
|
|
407
|
+
const packet = deserializePacket(Buffer.from(frameData));
|
|
408
|
+
this.receiveCallback?.(packet, conn.peerId);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Malformed packet — drop silently.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
conn.socket.on('close', () => {
|
|
417
|
+
if (conn.peerId && this.connections.get(conn.peerId) === conn) {
|
|
418
|
+
this.connections.delete(conn.peerId);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
conn.socket.on('error', () => {
|
|
422
|
+
// Error is followed by close; cleanup happens there.
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
// --------------------------------------------------------
|
|
426
|
+
// Peer Queries
|
|
427
|
+
// --------------------------------------------------------
|
|
428
|
+
/** Return the list of peers discovered via UDP announcements. */
|
|
429
|
+
getDiscoveredPeers() {
|
|
430
|
+
return Array.from(this.discoveredPeers.values()).map(({ id, address, port }) => ({
|
|
431
|
+
id,
|
|
432
|
+
address,
|
|
433
|
+
port,
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
/** Return the IDs of peers with an active TCP connection. */
|
|
437
|
+
getConnectedPeers() {
|
|
438
|
+
return Array.from(this.connections.keys());
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
export default LocalTransport;
|
|
442
|
+
//# sourceMappingURL=index.js.map
|