@antseed/node 0.1.0 → 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/LICENSE +674 -0
- package/README.md +7 -5
- package/dist/discovery/http-metadata-resolver.d.ts +6 -0
- package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
- package/dist/discovery/http-metadata-resolver.js +32 -4
- package/dist/discovery/http-metadata-resolver.js.map +1 -1
- package/dist/discovery/peer-lookup.d.ts +1 -0
- package/dist/discovery/peer-lookup.d.ts.map +1 -1
- package/dist/discovery/peer-lookup.js +10 -25
- package/dist/discovery/peer-lookup.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/seller-provider.d.ts +13 -1
- package/dist/interfaces/seller-provider.d.ts.map +1 -1
- package/dist/node.d.ts +13 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +146 -21
- package/dist/node.js.map +1 -1
- package/dist/proxy/proxy-mux.d.ts +3 -1
- package/dist/proxy/proxy-mux.d.ts.map +1 -1
- package/dist/proxy/proxy-mux.js +9 -5
- package/dist/proxy/proxy-mux.js.map +1 -1
- package/dist/types/http.d.ts +1 -0
- package/dist/types/http.d.ts.map +1 -1
- package/dist/types/http.js +1 -1
- package/dist/types/http.js.map +1 -1
- package/package.json +14 -10
- package/contracts/AntseedEscrow.sol +0 -310
- package/contracts/MockUSDC.sol +0 -64
- package/contracts/README.md +0 -102
- package/src/config/encryption.test.ts +0 -49
- package/src/config/encryption.ts +0 -53
- package/src/config/plugin-config-manager.test.ts +0 -92
- package/src/config/plugin-config-manager.ts +0 -153
- package/src/config/plugin-loader.ts +0 -90
- package/src/discovery/announcer.ts +0 -169
- package/src/discovery/bootstrap.ts +0 -57
- package/src/discovery/default-metadata-resolver.ts +0 -18
- package/src/discovery/dht-health.ts +0 -136
- package/src/discovery/dht-node.ts +0 -191
- package/src/discovery/http-metadata-resolver.ts +0 -47
- package/src/discovery/index.ts +0 -15
- package/src/discovery/metadata-codec.ts +0 -453
- package/src/discovery/metadata-resolver.ts +0 -7
- package/src/discovery/metadata-server.ts +0 -73
- package/src/discovery/metadata-validator.ts +0 -172
- package/src/discovery/peer-lookup.ts +0 -122
- package/src/discovery/peer-metadata.ts +0 -34
- package/src/discovery/peer-selector.ts +0 -134
- package/src/discovery/profile-manager.ts +0 -131
- package/src/discovery/profile-search.ts +0 -100
- package/src/discovery/reputation-verifier.ts +0 -54
- package/src/index.ts +0 -61
- package/src/interfaces/buyer-router.ts +0 -21
- package/src/interfaces/plugin.ts +0 -36
- package/src/interfaces/seller-provider.ts +0 -81
- package/src/metering/index.ts +0 -6
- package/src/metering/receipt-generator.ts +0 -105
- package/src/metering/receipt-verifier.ts +0 -102
- package/src/metering/session-tracker.ts +0 -145
- package/src/metering/storage.ts +0 -600
- package/src/metering/token-counter.ts +0 -127
- package/src/metering/usage-aggregator.ts +0 -236
- package/src/node.ts +0 -1698
- package/src/p2p/connection-auth.ts +0 -152
- package/src/p2p/connection-manager.ts +0 -916
- package/src/p2p/handshake.ts +0 -162
- package/src/p2p/ice-config.ts +0 -59
- package/src/p2p/identity.ts +0 -110
- package/src/p2p/index.ts +0 -11
- package/src/p2p/keepalive.ts +0 -118
- package/src/p2p/message-protocol.ts +0 -171
- package/src/p2p/nat-traversal.ts +0 -169
- package/src/p2p/payment-codec.ts +0 -165
- package/src/p2p/payment-mux.ts +0 -153
- package/src/p2p/reconnect.ts +0 -117
- package/src/payments/balance-manager.ts +0 -77
- package/src/payments/buyer-payment-manager.ts +0 -414
- package/src/payments/disputes.ts +0 -72
- package/src/payments/evm/escrow-client.ts +0 -263
- package/src/payments/evm/keypair.ts +0 -31
- package/src/payments/evm/signatures.ts +0 -103
- package/src/payments/evm/wallet.ts +0 -42
- package/src/payments/index.ts +0 -50
- package/src/payments/settlement.ts +0 -40
- package/src/payments/types.ts +0 -79
- package/src/proxy/index.ts +0 -3
- package/src/proxy/provider-detection.ts +0 -78
- package/src/proxy/proxy-mux.ts +0 -173
- package/src/proxy/request-codec.ts +0 -294
- package/src/reputation/index.ts +0 -6
- package/src/reputation/rating-manager.ts +0 -118
- package/src/reputation/report-manager.ts +0 -91
- package/src/reputation/trust-engine.ts +0 -120
- package/src/reputation/trust-score.ts +0 -74
- package/src/reputation/uptime-tracker.ts +0 -155
- package/src/routing/default-router.ts +0 -75
- package/src/types/bittorrent-dht.d.ts +0 -19
- package/src/types/buyer.ts +0 -37
- package/src/types/capability.ts +0 -34
- package/src/types/connection.ts +0 -29
- package/src/types/http.ts +0 -20
- package/src/types/index.ts +0 -14
- package/src/types/metering.ts +0 -175
- package/src/types/nat-api.d.ts +0 -29
- package/src/types/peer-profile.ts +0 -25
- package/src/types/peer.ts +0 -62
- package/src/types/plugin-config.ts +0 -31
- package/src/types/protocol.ts +0 -162
- package/src/types/provider.ts +0 -40
- package/src/types/rating.ts +0 -23
- package/src/types/report.ts +0 -30
- package/src/types/seller.ts +0 -38
- package/src/types/staking.ts +0 -23
- package/src/utils/debug.ts +0 -30
- package/src/utils/hex.ts +0 -14
- package/tests/balance-manager.test.ts +0 -156
- package/tests/bootstrap.test.ts +0 -108
- package/tests/buyer-payment-manager.test.ts +0 -358
- package/tests/connection-auth.test.ts +0 -87
- package/tests/default-router.test.ts +0 -148
- package/tests/evm-keypair.test.ts +0 -173
- package/tests/identity.test.ts +0 -133
- package/tests/message-protocol.test.ts +0 -212
- package/tests/metadata-codec.test.ts +0 -165
- package/tests/metadata-validator.test.ts +0 -261
- package/tests/metering-storage.test.ts +0 -244
- package/tests/payment-codec.test.ts +0 -95
- package/tests/payment-mux.test.ts +0 -191
- package/tests/peer-selector.test.ts +0 -184
- package/tests/provider-detection.test.ts +0 -107
- package/tests/proxy-mux-security.test.ts +0 -38
- package/tests/receipt.test.ts +0 -215
- package/tests/reputation-integration.test.ts +0 -195
- package/tests/request-codec.test.ts +0 -144
- package/tests/token-counter.test.ts +0 -122
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -7
|
@@ -1,916 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import net, { type Socket } from "node:net";
|
|
3
|
-
import type {
|
|
4
|
-
PeerConnection as NativeRtcPeerConnection,
|
|
5
|
-
DataChannel as NativeDataChannel,
|
|
6
|
-
DescriptionType as NativeDescriptionType,
|
|
7
|
-
} from "node-datachannel";
|
|
8
|
-
import { type PeerId } from "../types/peer.js";
|
|
9
|
-
import { ConnectionState, type ConnectionConfig } from "../types/connection.js";
|
|
10
|
-
import { type IceConfig, getDefaultIceConfig } from "./ice-config.js";
|
|
11
|
-
import {
|
|
12
|
-
type ConnectionAuthEnvelope,
|
|
13
|
-
NonceReplayGuard,
|
|
14
|
-
buildConnectionAuthEnvelope,
|
|
15
|
-
verifyConnectionAuthEnvelope,
|
|
16
|
-
} from "./connection-auth.js";
|
|
17
|
-
|
|
18
|
-
let _nodeDatachannel: typeof import("node-datachannel") | null = null;
|
|
19
|
-
|
|
20
|
-
async function loadNodeDatachannel(): Promise<typeof import("node-datachannel")> {
|
|
21
|
-
if (_nodeDatachannel) return _nodeDatachannel;
|
|
22
|
-
_nodeDatachannel = await import("node-datachannel");
|
|
23
|
-
return _nodeDatachannel;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getNodeDatachannel(): typeof import("node-datachannel") {
|
|
27
|
-
if (!_nodeDatachannel) throw new Error("node-datachannel not loaded");
|
|
28
|
-
return _nodeDatachannel;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface PeerEndpoint {
|
|
32
|
-
host: string;
|
|
33
|
-
port: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type TransportMode = "webrtc" | "tcp";
|
|
37
|
-
type InitialWireMessage =
|
|
38
|
-
| {
|
|
39
|
-
type: "intro";
|
|
40
|
-
auth: ConnectionAuthEnvelope;
|
|
41
|
-
}
|
|
42
|
-
| {
|
|
43
|
-
type: "hello";
|
|
44
|
-
auth: ConnectionAuthEnvelope;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
type SignalingMessage =
|
|
48
|
-
| {
|
|
49
|
-
type: "sdp";
|
|
50
|
-
sdp: string;
|
|
51
|
-
descriptionType: NativeDescriptionType;
|
|
52
|
-
}
|
|
53
|
-
| {
|
|
54
|
-
type: "candidate";
|
|
55
|
-
candidate: string;
|
|
56
|
-
mid: string;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const DATA_CHANNEL_LABEL = "antseed-data";
|
|
60
|
-
const LINE_SEPARATOR = "\n";
|
|
61
|
-
const INITIAL_LINE_TIMEOUT_MS = 10_000;
|
|
62
|
-
const MAX_INITIAL_LINE_BYTES = 8 * 1024;
|
|
63
|
-
|
|
64
|
-
/** Represents a single P2P connection. */
|
|
65
|
-
export class PeerConnection extends EventEmitter {
|
|
66
|
-
readonly remotePeerId: PeerId;
|
|
67
|
-
readonly isInitiator: boolean;
|
|
68
|
-
private _state: ConnectionState = ConnectionState.Connecting;
|
|
69
|
-
private _timeoutMs: number;
|
|
70
|
-
private _timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
71
|
-
private _rtc: NativeRtcPeerConnection | null = null;
|
|
72
|
-
private _dataChannel: NativeDataChannel | null = null;
|
|
73
|
-
private _rawSocket: Socket | null = null;
|
|
74
|
-
private _signalingSocket: Socket | null = null;
|
|
75
|
-
|
|
76
|
-
constructor(config: ConnectionConfig) {
|
|
77
|
-
super();
|
|
78
|
-
this.remotePeerId = config.remotePeerId;
|
|
79
|
-
this.isInitiator = config.isInitiator;
|
|
80
|
-
this._timeoutMs = config.timeoutMs ?? 30_000;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get state(): ConnectionState {
|
|
84
|
-
return this._state;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
attachRtcPeer(rtc: NativeRtcPeerConnection): void {
|
|
88
|
-
this._rtc = rtc;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
attachSignalingSocket(socket: Socket): void {
|
|
92
|
-
this._signalingSocket = socket;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
attachDataChannel(channel: NativeDataChannel): void {
|
|
96
|
-
this._dataChannel = channel;
|
|
97
|
-
|
|
98
|
-
channel.onOpen(() => {
|
|
99
|
-
this.clearTimeout();
|
|
100
|
-
if (this._state === ConnectionState.Connecting) {
|
|
101
|
-
this.setState(ConnectionState.Open);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
channel.onClosed(() => {
|
|
106
|
-
if (this._state !== ConnectionState.Closed && this._state !== ConnectionState.Failed) {
|
|
107
|
-
this.setState(ConnectionState.Closed);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
channel.onError((err: string) => {
|
|
112
|
-
this.fail(new Error(`DataChannel error: ${err}`));
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
channel.onMessage((msg: string | Buffer) => {
|
|
116
|
-
if (typeof msg === "string") {
|
|
117
|
-
this.emit("message", new TextEncoder().encode(msg));
|
|
118
|
-
} else {
|
|
119
|
-
this.emit("message", new Uint8Array(msg));
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
attachRawSocket(socket: Socket, initialData?: Uint8Array): void {
|
|
125
|
-
this._rawSocket = socket;
|
|
126
|
-
|
|
127
|
-
socket.on("data", (chunk: Buffer) => {
|
|
128
|
-
this.emit("message", new Uint8Array(chunk));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
socket.on("error", (err: Error) => {
|
|
132
|
-
this.fail(err);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
socket.on("close", () => {
|
|
136
|
-
if (this._state !== ConnectionState.Closed && this._state !== ConnectionState.Failed) {
|
|
137
|
-
this.setState(ConnectionState.Closed);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
this.clearTimeout();
|
|
142
|
-
if (this._state === ConnectionState.Connecting) {
|
|
143
|
-
this.setState(ConnectionState.Open);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (initialData && initialData.length > 0) {
|
|
147
|
-
queueMicrotask(() => {
|
|
148
|
-
this.emit("message", initialData);
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
fail(err: Error): void {
|
|
154
|
-
if (this._state !== ConnectionState.Failed && this._state !== ConnectionState.Closed) {
|
|
155
|
-
this.setState(ConnectionState.Failed);
|
|
156
|
-
}
|
|
157
|
-
this._teardownTransports();
|
|
158
|
-
this._emitError(err);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Transition to a new state and emit event. */
|
|
162
|
-
setState(newState: ConnectionState): void {
|
|
163
|
-
if (this._state === newState) return;
|
|
164
|
-
this._state = newState;
|
|
165
|
-
this.emit("stateChange", newState);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Start the connection timeout. */
|
|
169
|
-
startTimeout(): void {
|
|
170
|
-
this._timeoutHandle = setTimeout(() => {
|
|
171
|
-
if (this._state === ConnectionState.Connecting) {
|
|
172
|
-
this.fail(new Error(`Connection to ${this.remotePeerId} timed out`));
|
|
173
|
-
}
|
|
174
|
-
}, this._timeoutMs);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Clear the connection timeout. */
|
|
178
|
-
clearTimeout(): void {
|
|
179
|
-
if (this._timeoutHandle) {
|
|
180
|
-
clearTimeout(this._timeoutHandle);
|
|
181
|
-
this._timeoutHandle = null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Send a message through the active transport. */
|
|
186
|
-
send(data: Uint8Array): void {
|
|
187
|
-
if (this._state !== ConnectionState.Open && this._state !== ConnectionState.Authenticated) {
|
|
188
|
-
throw new Error(`Cannot send in state ${this._state}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (this._dataChannel && this._dataChannel.isOpen()) {
|
|
192
|
-
const ok = this._dataChannel.sendMessageBinary(data);
|
|
193
|
-
if (!ok) {
|
|
194
|
-
throw new Error(`Failed to send data to ${this.remotePeerId}`);
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!this._rawSocket || this._rawSocket.destroyed || !this._rawSocket.writable) {
|
|
200
|
-
throw new Error(`Cannot send to ${this.remotePeerId}: no writable transport`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
this._rawSocket.write(Buffer.from(data));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Close the connection gracefully. */
|
|
207
|
-
close(): void {
|
|
208
|
-
if (this._state === ConnectionState.Closed) {
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
this.clearTimeout();
|
|
213
|
-
this.setState(ConnectionState.Closing);
|
|
214
|
-
this._teardownTransports();
|
|
215
|
-
this.setState(ConnectionState.Closed);
|
|
216
|
-
this.removeAllListeners();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private _teardownTransports(): void {
|
|
220
|
-
if (this._dataChannel) {
|
|
221
|
-
try {
|
|
222
|
-
this._dataChannel.close();
|
|
223
|
-
} catch {
|
|
224
|
-
// best effort close
|
|
225
|
-
}
|
|
226
|
-
this._dataChannel = null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (this._rtc) {
|
|
230
|
-
try {
|
|
231
|
-
this._rtc.close();
|
|
232
|
-
} catch {
|
|
233
|
-
// best effort close
|
|
234
|
-
}
|
|
235
|
-
this._rtc = null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (this._rawSocket && !this._rawSocket.destroyed) {
|
|
239
|
-
this._rawSocket.destroy();
|
|
240
|
-
}
|
|
241
|
-
this._rawSocket = null;
|
|
242
|
-
|
|
243
|
-
if (this._signalingSocket && !this._signalingSocket.destroyed) {
|
|
244
|
-
this._signalingSocket.destroy();
|
|
245
|
-
}
|
|
246
|
-
this._signalingSocket = null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private _emitError(err: Error): void {
|
|
250
|
-
if (this.listenerCount("error") > 0) {
|
|
251
|
-
this.emit("error", err);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Manages all peer connections and optional inbound listening. */
|
|
257
|
-
export class ConnectionManager extends EventEmitter {
|
|
258
|
-
private _connections = new Map<PeerId, PeerConnection>();
|
|
259
|
-
private _iceConfig: IceConfig;
|
|
260
|
-
private _localPeerId: PeerId | null = null;
|
|
261
|
-
private _localPrivateKey: Uint8Array | null = null;
|
|
262
|
-
private _listenHost = "127.0.0.1";
|
|
263
|
-
private _listenPort: number | null = null;
|
|
264
|
-
private _server: net.Server | null = null;
|
|
265
|
-
private _transportMode: TransportMode;
|
|
266
|
-
private _metadataProvider: (() => object | null) | null = null;
|
|
267
|
-
private _ipConnectionCounts = new Map<string, number>();
|
|
268
|
-
private readonly _introReplayGuard = new NonceReplayGuard();
|
|
269
|
-
private static _knownEndpoints = new Map<PeerId, PeerEndpoint>();
|
|
270
|
-
private static _detectedTransportMode: TransportMode | null = null;
|
|
271
|
-
|
|
272
|
-
constructor(iceConfig?: IceConfig) {
|
|
273
|
-
super();
|
|
274
|
-
this._iceConfig = iceConfig ?? getDefaultIceConfig();
|
|
275
|
-
this._transportMode = ConnectionManager._detectTransportMode();
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
static async init(iceConfig?: IceConfig): Promise<ConnectionManager> {
|
|
279
|
-
try {
|
|
280
|
-
await loadNodeDatachannel();
|
|
281
|
-
} catch {
|
|
282
|
-
// node-datachannel not available — TCP fallback will be used
|
|
283
|
-
}
|
|
284
|
-
return new ConnectionManager(iceConfig);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
get iceConfig(): IceConfig {
|
|
288
|
-
return this._iceConfig;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
get connections(): ReadonlyMap<PeerId, PeerConnection> {
|
|
292
|
-
return this._connections;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
getListeningPort(): number | null {
|
|
296
|
-
return this._listenPort;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
setMetadataProvider(provider: () => object | null): void {
|
|
300
|
-
this._metadataProvider = provider;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
setLocalPeerId(peerId: PeerId): void {
|
|
304
|
-
this._localPeerId = peerId;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
setLocalIdentity(identity: { peerId: PeerId; privateKey: Uint8Array }): void {
|
|
308
|
-
this._localPeerId = identity.peerId;
|
|
309
|
-
this._localPrivateKey = identity.privateKey;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
static registerPeerEndpoint(peerId: PeerId, endpoint: PeerEndpoint): void {
|
|
313
|
-
this._knownEndpoints.set(peerId, endpoint);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
static resolvePeerEndpoint(peerId: PeerId): PeerEndpoint | undefined {
|
|
317
|
-
return this._knownEndpoints.get(peerId);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
registerPeerEndpoint(peerId: PeerId, endpoint: PeerEndpoint): void {
|
|
321
|
-
ConnectionManager.registerPeerEndpoint(peerId, endpoint);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async startListening(config: { peerId: PeerId; port: number; host?: string }): Promise<void> {
|
|
325
|
-
this._localPeerId = config.peerId;
|
|
326
|
-
this._listenHost = config.host ?? "127.0.0.1";
|
|
327
|
-
this._listenPort = config.port;
|
|
328
|
-
|
|
329
|
-
if (this._server) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
this._server = net.createServer((socket) => {
|
|
334
|
-
const ip = socket.remoteAddress ?? 'unknown';
|
|
335
|
-
const current = this._ipConnectionCounts.get(ip) ?? 0;
|
|
336
|
-
if (current >= 10) {
|
|
337
|
-
socket.destroy();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
this._ipConnectionCounts.set(ip, current + 1);
|
|
341
|
-
socket.once('close', () => {
|
|
342
|
-
const count = this._ipConnectionCounts.get(ip) ?? 1;
|
|
343
|
-
if (count <= 1) {
|
|
344
|
-
this._ipConnectionCounts.delete(ip);
|
|
345
|
-
} else {
|
|
346
|
-
this._ipConnectionCounts.set(ip, count - 1);
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
this._handleInboundSocket(socket);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
this._server.maxConnections = 256;
|
|
353
|
-
|
|
354
|
-
await new Promise<void>((resolve, reject) => {
|
|
355
|
-
this._server!.once("error", reject);
|
|
356
|
-
this._server!.listen(this._listenPort!, this._listenHost, () => resolve());
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// Resolve actual bound port (important when port 0 is used for OS-assigned)
|
|
360
|
-
const addr = this._server.address();
|
|
361
|
-
if (addr && typeof addr !== 'string') {
|
|
362
|
-
this._listenPort = addr.port;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
ConnectionManager.registerPeerEndpoint(config.peerId, {
|
|
366
|
-
host: this._listenHost,
|
|
367
|
-
port: this._listenPort,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async stopListening(): Promise<void> {
|
|
372
|
-
if (!this._server) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const server = this._server;
|
|
377
|
-
this._server = null;
|
|
378
|
-
|
|
379
|
-
await new Promise<void>((resolve) => {
|
|
380
|
-
server.close(() => resolve());
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
if (this._localPeerId) {
|
|
384
|
-
ConnectionManager._knownEndpoints.delete(this._localPeerId);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** Create a new outbound connection. */
|
|
389
|
-
createConnection(config: ConnectionConfig): PeerConnection {
|
|
390
|
-
const existing = this._connections.get(config.remotePeerId);
|
|
391
|
-
if (existing && existing.state !== ConnectionState.Closed && existing.state !== ConnectionState.Failed) {
|
|
392
|
-
throw new Error(`Connection to ${config.remotePeerId} already exists`);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const conn = new PeerConnection(config);
|
|
396
|
-
this._registerConnection(config.remotePeerId, conn);
|
|
397
|
-
conn.startTimeout();
|
|
398
|
-
|
|
399
|
-
if (!this._localPeerId) {
|
|
400
|
-
queueMicrotask(() => {
|
|
401
|
-
conn.fail(new Error("Local peer id is not configured"));
|
|
402
|
-
});
|
|
403
|
-
return conn;
|
|
404
|
-
}
|
|
405
|
-
if (!this._localPrivateKey) {
|
|
406
|
-
queueMicrotask(() => {
|
|
407
|
-
conn.fail(new Error("Local private key is not configured"));
|
|
408
|
-
});
|
|
409
|
-
return conn;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const endpoint = config.endpoint ?? ConnectionManager.resolvePeerEndpoint(config.remotePeerId);
|
|
413
|
-
if (!endpoint) {
|
|
414
|
-
queueMicrotask(() => {
|
|
415
|
-
conn.fail(new Error(`No endpoint registered for peer ${config.remotePeerId}`));
|
|
416
|
-
});
|
|
417
|
-
return conn;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
ConnectionManager.registerPeerEndpoint(config.remotePeerId, endpoint);
|
|
421
|
-
|
|
422
|
-
if (this._transportMode === "webrtc") {
|
|
423
|
-
this._createWebRtcConnection(config, conn, endpoint);
|
|
424
|
-
} else {
|
|
425
|
-
this._createTcpConnection(config, conn, endpoint);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return conn;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/** Get an existing connection by peer ID. */
|
|
432
|
-
getConnection(peerId: PeerId): PeerConnection | undefined {
|
|
433
|
-
return this._connections.get(peerId);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** Close a specific connection. */
|
|
437
|
-
closeConnection(peerId: PeerId): void {
|
|
438
|
-
const conn = this._connections.get(peerId);
|
|
439
|
-
if (conn) {
|
|
440
|
-
conn.close();
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/** Close all connections and clean up. */
|
|
445
|
-
closeAll(): void {
|
|
446
|
-
for (const conn of this._connections.values()) {
|
|
447
|
-
conn.close();
|
|
448
|
-
}
|
|
449
|
-
this._connections.clear();
|
|
450
|
-
if (this._server) {
|
|
451
|
-
void this.stopListening();
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private _createWebRtcConnection(
|
|
456
|
-
config: ConnectionConfig,
|
|
457
|
-
conn: PeerConnection,
|
|
458
|
-
endpoint: PeerEndpoint,
|
|
459
|
-
): void {
|
|
460
|
-
const signalingSocket = net.connect({ host: endpoint.host, port: endpoint.port });
|
|
461
|
-
conn.attachSignalingSocket(signalingSocket);
|
|
462
|
-
|
|
463
|
-
let rtc: NativeRtcPeerConnection | null = null;
|
|
464
|
-
const pendingSignals: SignalingMessage[] = [];
|
|
465
|
-
|
|
466
|
-
this._attachSignalingParser(
|
|
467
|
-
signalingSocket,
|
|
468
|
-
(msg) => {
|
|
469
|
-
if (!rtc) {
|
|
470
|
-
pendingSignals.push(msg);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
this._applySignalToRtc(rtc, msg, conn);
|
|
474
|
-
},
|
|
475
|
-
(err) => conn.fail(err),
|
|
476
|
-
"",
|
|
477
|
-
);
|
|
478
|
-
|
|
479
|
-
signalingSocket.once("connect", () => {
|
|
480
|
-
this._sendLine(signalingSocket, {
|
|
481
|
-
type: "hello",
|
|
482
|
-
auth: buildConnectionAuthEnvelope(
|
|
483
|
-
"hello",
|
|
484
|
-
this._localPeerId!,
|
|
485
|
-
this._localPrivateKey!,
|
|
486
|
-
),
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
rtc = this._createRtcPeer(config.remotePeerId);
|
|
490
|
-
conn.attachRtcPeer(rtc);
|
|
491
|
-
this._wireRtcPeer(conn, rtc, signalingSocket, true);
|
|
492
|
-
|
|
493
|
-
for (const signal of pendingSignals) {
|
|
494
|
-
this._applySignalToRtc(rtc, signal, conn);
|
|
495
|
-
}
|
|
496
|
-
pendingSignals.length = 0;
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
signalingSocket.on("error", (err: Error) => {
|
|
500
|
-
if (conn.state === ConnectionState.Connecting) {
|
|
501
|
-
conn.fail(err);
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
signalingSocket.on("close", () => {
|
|
506
|
-
if (conn.state === ConnectionState.Connecting) {
|
|
507
|
-
conn.fail(new Error(`Signaling socket closed before connection to ${config.remotePeerId} opened`));
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
private _createTcpConnection(
|
|
513
|
-
config: ConnectionConfig,
|
|
514
|
-
conn: PeerConnection,
|
|
515
|
-
endpoint: PeerEndpoint,
|
|
516
|
-
): void {
|
|
517
|
-
const socket = net.connect({ host: endpoint.host, port: endpoint.port });
|
|
518
|
-
|
|
519
|
-
socket.once("connect", () => {
|
|
520
|
-
this._sendLine(socket, {
|
|
521
|
-
type: "intro",
|
|
522
|
-
auth: buildConnectionAuthEnvelope(
|
|
523
|
-
"intro",
|
|
524
|
-
this._localPeerId!,
|
|
525
|
-
this._localPrivateKey!,
|
|
526
|
-
),
|
|
527
|
-
});
|
|
528
|
-
conn.attachRawSocket(socket);
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
socket.on("error", (err: Error) => {
|
|
532
|
-
if (conn.state === ConnectionState.Connecting) {
|
|
533
|
-
conn.fail(err);
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
socket.on("close", () => {
|
|
538
|
-
if (conn.state === ConnectionState.Connecting) {
|
|
539
|
-
conn.fail(new Error(`TCP socket closed before connection to ${config.remotePeerId} opened`));
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
private _handleInboundSocket(socket: Socket): void {
|
|
545
|
-
let buffer = Buffer.alloc(0);
|
|
546
|
-
const timeout = setTimeout(() => {
|
|
547
|
-
socket.destroy(new Error("Inbound socket intro timeout"));
|
|
548
|
-
}, INITIAL_LINE_TIMEOUT_MS);
|
|
549
|
-
|
|
550
|
-
const onData = (chunk: Buffer): void => {
|
|
551
|
-
if (buffer.length + chunk.length > MAX_INITIAL_LINE_BYTES) {
|
|
552
|
-
socket.off("data", onData);
|
|
553
|
-
clearTimeout(timeout);
|
|
554
|
-
socket.destroy(new Error("Inbound socket intro exceeded 8KB limit"));
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
558
|
-
const lineBreak = buffer.indexOf(0x0a); // '\n'
|
|
559
|
-
if (lineBreak < 0) {
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
socket.off("data", onData);
|
|
564
|
-
clearTimeout(timeout);
|
|
565
|
-
|
|
566
|
-
const line = buffer.subarray(0, lineBreak).toString("utf8").trim();
|
|
567
|
-
const remaining = buffer.subarray(lineBreak + 1);
|
|
568
|
-
|
|
569
|
-
// Detect HTTP requests (metadata endpoint served on signaling port)
|
|
570
|
-
if (line.startsWith("GET ") || line.startsWith("HEAD ")) {
|
|
571
|
-
this._serveHttpMetadata(socket, line);
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
let intro: InitialWireMessage;
|
|
576
|
-
try {
|
|
577
|
-
intro = JSON.parse(line) as InitialWireMessage;
|
|
578
|
-
} catch {
|
|
579
|
-
socket.destroy(new Error("Failed to parse initial socket message"));
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
if (intro.type !== "intro" && intro.type !== "hello") {
|
|
583
|
-
socket.destroy(new Error("Unsupported initial socket message type"));
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const verified = verifyConnectionAuthEnvelope({
|
|
588
|
-
type: intro.type,
|
|
589
|
-
auth: intro.auth,
|
|
590
|
-
replayGuard: this._introReplayGuard,
|
|
591
|
-
});
|
|
592
|
-
if (!verified.ok || !verified.peerId) {
|
|
593
|
-
socket.destroy(new Error(`Inbound auth failed: ${verified.reason ?? "unknown reason"}`));
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
if (intro.type === "intro") {
|
|
598
|
-
this._acceptTcpInbound(socket, verified.peerId, remaining);
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (intro.type === "hello") {
|
|
603
|
-
this._acceptWebRtcInbound(socket, verified.peerId, remaining.toString("utf8"));
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
socket.destroy(new Error("Unsupported initial socket message type"));
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
socket.on("data", onData);
|
|
611
|
-
socket.on("error", (err: Error) => {
|
|
612
|
-
clearTimeout(timeout);
|
|
613
|
-
this._emitError(err);
|
|
614
|
-
});
|
|
615
|
-
socket.on("close", () => {
|
|
616
|
-
clearTimeout(timeout);
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
private _serveHttpMetadata(socket: Socket, requestLine: string): void {
|
|
621
|
-
const MAX_HEADER_SIZE = 8 * 1024; // 8KB
|
|
622
|
-
let headerBytes = 0;
|
|
623
|
-
const headerTimeout = setTimeout(() => {
|
|
624
|
-
socket.destroy(new Error("HTTP header read timeout"));
|
|
625
|
-
}, 5_000);
|
|
626
|
-
|
|
627
|
-
const onData = (chunk: Buffer): void => {
|
|
628
|
-
headerBytes += chunk.length;
|
|
629
|
-
if (headerBytes > MAX_HEADER_SIZE) {
|
|
630
|
-
clearTimeout(headerTimeout);
|
|
631
|
-
socket.off("data", onData);
|
|
632
|
-
socket.destroy(new Error("HTTP headers exceeded 8KB limit"));
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
if (chunk.includes(Buffer.from("\r\n\r\n")) || chunk.includes(Buffer.from("\n\n"))) {
|
|
636
|
-
clearTimeout(headerTimeout);
|
|
637
|
-
socket.off("data", onData);
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
socket.on("data", onData);
|
|
641
|
-
|
|
642
|
-
const url = requestLine.split(" ")[1] ?? "";
|
|
643
|
-
let statusLine: string;
|
|
644
|
-
let body: string;
|
|
645
|
-
|
|
646
|
-
if (url !== "/metadata") {
|
|
647
|
-
statusLine = "404 Not Found";
|
|
648
|
-
body = JSON.stringify({ error: "not found" });
|
|
649
|
-
} else if (!this._metadataProvider) {
|
|
650
|
-
statusLine = "503 Service Unavailable";
|
|
651
|
-
body = JSON.stringify({ error: "metadata not available" });
|
|
652
|
-
} else {
|
|
653
|
-
const metadata = this._metadataProvider();
|
|
654
|
-
if (!metadata) {
|
|
655
|
-
statusLine = "503 Service Unavailable";
|
|
656
|
-
body = JSON.stringify({ error: "metadata not available" });
|
|
657
|
-
} else {
|
|
658
|
-
statusLine = "200 OK";
|
|
659
|
-
body = JSON.stringify(metadata);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
socket.end(
|
|
664
|
-
`HTTP/1.1 ${statusLine}\r\n` +
|
|
665
|
-
`Content-Type: application/json\r\n` +
|
|
666
|
-
`Content-Length: ${Buffer.byteLength(body)}\r\n` +
|
|
667
|
-
`Connection: close\r\n` +
|
|
668
|
-
`\r\n` +
|
|
669
|
-
body,
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
private _acceptTcpInbound(socket: Socket, remotePeerId: PeerId, remainingData: Buffer): void {
|
|
674
|
-
const existing = this._connections.get(remotePeerId);
|
|
675
|
-
if (existing && existing.state !== ConnectionState.Closed && existing.state !== ConnectionState.Failed) {
|
|
676
|
-
socket.destroy(new Error(`Connection from ${remotePeerId} already exists`));
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const conn = new PeerConnection({
|
|
681
|
-
remotePeerId,
|
|
682
|
-
isInitiator: false,
|
|
683
|
-
});
|
|
684
|
-
this._registerConnection(remotePeerId, conn);
|
|
685
|
-
conn.attachRawSocket(
|
|
686
|
-
socket,
|
|
687
|
-
remainingData.length > 0 ? new Uint8Array(remainingData) : undefined,
|
|
688
|
-
);
|
|
689
|
-
this.emit("connection", conn);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
private _acceptWebRtcInbound(socket: Socket, remotePeerId: PeerId, initialSignalingBuffer: string): void {
|
|
693
|
-
const existing = this._connections.get(remotePeerId);
|
|
694
|
-
if (existing && existing.state !== ConnectionState.Closed && existing.state !== ConnectionState.Failed) {
|
|
695
|
-
socket.destroy(new Error(`Connection from ${remotePeerId} already exists`));
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const conn = new PeerConnection({
|
|
700
|
-
remotePeerId,
|
|
701
|
-
isInitiator: false,
|
|
702
|
-
});
|
|
703
|
-
conn.attachSignalingSocket(socket);
|
|
704
|
-
this._registerConnection(remotePeerId, conn);
|
|
705
|
-
|
|
706
|
-
const rtc = this._createRtcPeer(remotePeerId);
|
|
707
|
-
conn.attachRtcPeer(rtc);
|
|
708
|
-
this._wireRtcPeer(conn, rtc, socket, false);
|
|
709
|
-
|
|
710
|
-
this._attachSignalingParser(
|
|
711
|
-
socket,
|
|
712
|
-
(msg) => {
|
|
713
|
-
this._applySignalToRtc(rtc, msg, conn);
|
|
714
|
-
},
|
|
715
|
-
(err) => conn.fail(err),
|
|
716
|
-
initialSignalingBuffer,
|
|
717
|
-
);
|
|
718
|
-
|
|
719
|
-
socket.on("close", () => {
|
|
720
|
-
if (conn.state === ConnectionState.Connecting) {
|
|
721
|
-
conn.fail(new Error(`Inbound signaling from ${remotePeerId} closed before connection opened`));
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
socket.on("error", (err: Error) => {
|
|
726
|
-
conn.fail(err);
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
this.emit("connection", conn);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
private _wireRtcPeer(
|
|
733
|
-
conn: PeerConnection,
|
|
734
|
-
rtc: NativeRtcPeerConnection,
|
|
735
|
-
signalingSocket: Socket,
|
|
736
|
-
isInitiator: boolean,
|
|
737
|
-
): void {
|
|
738
|
-
rtc.onLocalDescription((sdp: string, descriptionType: string) => {
|
|
739
|
-
this._sendLine(signalingSocket, {
|
|
740
|
-
type: "sdp",
|
|
741
|
-
sdp,
|
|
742
|
-
descriptionType: this._normalizeDescriptionType(descriptionType),
|
|
743
|
-
});
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
rtc.onLocalCandidate((candidate: string, mid: string) => {
|
|
747
|
-
this._sendLine(signalingSocket, {
|
|
748
|
-
type: "candidate",
|
|
749
|
-
candidate,
|
|
750
|
-
mid,
|
|
751
|
-
});
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
rtc.onStateChange((state: string) => {
|
|
755
|
-
const lower = state.toLowerCase();
|
|
756
|
-
if (lower === "failed" || lower === "disconnected" || lower === "closed") {
|
|
757
|
-
if (conn.state === ConnectionState.Connecting || conn.state === ConnectionState.Open) {
|
|
758
|
-
conn.fail(new Error(`WebRTC state is ${state}`));
|
|
759
|
-
} else if (conn.state === ConnectionState.Authenticated) {
|
|
760
|
-
conn.close();
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
if (isInitiator) {
|
|
766
|
-
const channel = rtc.createDataChannel(DATA_CHANNEL_LABEL, { ordered: true });
|
|
767
|
-
conn.attachDataChannel(channel);
|
|
768
|
-
rtc.setLocalDescription();
|
|
769
|
-
} else {
|
|
770
|
-
rtc.onDataChannel((channel: NativeDataChannel) => {
|
|
771
|
-
conn.attachDataChannel(channel);
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
private _createRtcPeer(remotePeerId: PeerId): NativeRtcPeerConnection {
|
|
777
|
-
const ndc = getNodeDatachannel();
|
|
778
|
-
const iceServers = this._iceConfig.iceServers.flatMap((server) => {
|
|
779
|
-
return Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
return new ndc.PeerConnection(`idleai-${remotePeerId.slice(0, 12)}`, {
|
|
783
|
-
iceServers,
|
|
784
|
-
iceTransportPolicy: this._iceConfig.iceTransportPolicy ?? "all",
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
private _applySignalToRtc(
|
|
789
|
-
rtc: NativeRtcPeerConnection,
|
|
790
|
-
signal: SignalingMessage,
|
|
791
|
-
conn: PeerConnection,
|
|
792
|
-
): void {
|
|
793
|
-
try {
|
|
794
|
-
if (signal.type === "sdp") {
|
|
795
|
-
rtc.setRemoteDescription(signal.sdp, signal.descriptionType);
|
|
796
|
-
} else {
|
|
797
|
-
rtc.addRemoteCandidate(signal.candidate, signal.mid);
|
|
798
|
-
}
|
|
799
|
-
} catch (err) {
|
|
800
|
-
conn.fail(err instanceof Error ? err : new Error(String(err)));
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
private _attachSignalingParser(
|
|
805
|
-
socket: Socket,
|
|
806
|
-
onMessage: (msg: SignalingMessage) => void,
|
|
807
|
-
onError: (err: Error) => void,
|
|
808
|
-
initialBuffer: string,
|
|
809
|
-
): void {
|
|
810
|
-
const MAX_BUFFER_SIZE = 64 * 1024; // 64KB
|
|
811
|
-
let buffer = initialBuffer;
|
|
812
|
-
|
|
813
|
-
const processBuffer = (): void => {
|
|
814
|
-
while (true) {
|
|
815
|
-
const lineBreak = buffer.indexOf(LINE_SEPARATOR);
|
|
816
|
-
if (lineBreak < 0) {
|
|
817
|
-
break;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const line = buffer.slice(0, lineBreak).trim();
|
|
821
|
-
buffer = buffer.slice(lineBreak + LINE_SEPARATOR.length);
|
|
822
|
-
if (line.length === 0) {
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
try {
|
|
827
|
-
const parsed = JSON.parse(line) as SignalingMessage;
|
|
828
|
-
onMessage(parsed);
|
|
829
|
-
} catch (err) {
|
|
830
|
-
onError(err instanceof Error ? err : new Error(String(err)));
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
|
|
836
|
-
if (buffer.length > 0) {
|
|
837
|
-
processBuffer();
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
socket.on("data", (chunk: Buffer) => {
|
|
841
|
-
buffer += chunk.toString("utf8");
|
|
842
|
-
if (buffer.length > MAX_BUFFER_SIZE) {
|
|
843
|
-
socket.destroy(new Error("Signaling buffer exceeded 64KB limit"));
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
processBuffer();
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
private _sendLine(socket: Socket, payload: object): void {
|
|
851
|
-
if (socket.destroyed) {
|
|
852
|
-
throw new Error("Cannot send message on destroyed socket");
|
|
853
|
-
}
|
|
854
|
-
socket.write(JSON.stringify(payload) + LINE_SEPARATOR);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
private _registerConnection(peerId: PeerId, conn: PeerConnection): void {
|
|
858
|
-
this._connections.set(peerId, conn);
|
|
859
|
-
|
|
860
|
-
conn.on("stateChange", (state: ConnectionState) => {
|
|
861
|
-
this.emit("connectionStateChange", peerId, state);
|
|
862
|
-
if (state === ConnectionState.Closed || state === ConnectionState.Failed) {
|
|
863
|
-
this._connections.delete(peerId);
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
conn.on("error", (err: Error) => {
|
|
868
|
-
this._emitError(err);
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
private static _detectTransportMode(): TransportMode {
|
|
873
|
-
if (this._detectedTransportMode) {
|
|
874
|
-
return this._detectedTransportMode;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
try {
|
|
878
|
-
const ndc = _nodeDatachannel;
|
|
879
|
-
if (!ndc) {
|
|
880
|
-
this._detectedTransportMode = "tcp";
|
|
881
|
-
return this._detectedTransportMode;
|
|
882
|
-
}
|
|
883
|
-
const probe = new ndc.PeerConnection("idleai-transport-probe", { iceServers: [] });
|
|
884
|
-
try {
|
|
885
|
-
const channel = probe.createDataChannel("probe", { ordered: true });
|
|
886
|
-
channel.close();
|
|
887
|
-
} finally {
|
|
888
|
-
probe.close();
|
|
889
|
-
}
|
|
890
|
-
this._detectedTransportMode = "webrtc";
|
|
891
|
-
} catch {
|
|
892
|
-
this._detectedTransportMode = "tcp";
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
return this._detectedTransportMode;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
private _normalizeDescriptionType(type: string): NativeDescriptionType {
|
|
899
|
-
switch (type) {
|
|
900
|
-
case "offer":
|
|
901
|
-
case "answer":
|
|
902
|
-
case "pranswer":
|
|
903
|
-
case "rollback":
|
|
904
|
-
case "unspec":
|
|
905
|
-
return type as NativeDescriptionType;
|
|
906
|
-
default:
|
|
907
|
-
return "unspec" as NativeDescriptionType;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
private _emitError(err: Error): void {
|
|
912
|
-
if (this.listenerCount("error") > 0) {
|
|
913
|
-
this.emit("error", err);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|