@antseed/node 0.1.0 → 0.1.1

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 (130) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/interfaces/seller-provider.d.ts +13 -1
  6. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  7. package/dist/node.d.ts +13 -3
  8. package/dist/node.d.ts.map +1 -1
  9. package/dist/node.js +123 -15
  10. package/dist/node.js.map +1 -1
  11. package/dist/proxy/proxy-mux.d.ts +3 -1
  12. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  13. package/dist/proxy/proxy-mux.js +9 -5
  14. package/dist/proxy/proxy-mux.js.map +1 -1
  15. package/dist/types/http.d.ts +1 -0
  16. package/dist/types/http.d.ts.map +1 -1
  17. package/dist/types/http.js +1 -1
  18. package/dist/types/http.js.map +1 -1
  19. package/package.json +14 -10
  20. package/contracts/AntseedEscrow.sol +0 -310
  21. package/contracts/MockUSDC.sol +0 -64
  22. package/contracts/README.md +0 -102
  23. package/src/config/encryption.test.ts +0 -49
  24. package/src/config/encryption.ts +0 -53
  25. package/src/config/plugin-config-manager.test.ts +0 -92
  26. package/src/config/plugin-config-manager.ts +0 -153
  27. package/src/config/plugin-loader.ts +0 -90
  28. package/src/discovery/announcer.ts +0 -169
  29. package/src/discovery/bootstrap.ts +0 -57
  30. package/src/discovery/default-metadata-resolver.ts +0 -18
  31. package/src/discovery/dht-health.ts +0 -136
  32. package/src/discovery/dht-node.ts +0 -191
  33. package/src/discovery/http-metadata-resolver.ts +0 -47
  34. package/src/discovery/index.ts +0 -15
  35. package/src/discovery/metadata-codec.ts +0 -453
  36. package/src/discovery/metadata-resolver.ts +0 -7
  37. package/src/discovery/metadata-server.ts +0 -73
  38. package/src/discovery/metadata-validator.ts +0 -172
  39. package/src/discovery/peer-lookup.ts +0 -122
  40. package/src/discovery/peer-metadata.ts +0 -34
  41. package/src/discovery/peer-selector.ts +0 -134
  42. package/src/discovery/profile-manager.ts +0 -131
  43. package/src/discovery/profile-search.ts +0 -100
  44. package/src/discovery/reputation-verifier.ts +0 -54
  45. package/src/index.ts +0 -61
  46. package/src/interfaces/buyer-router.ts +0 -21
  47. package/src/interfaces/plugin.ts +0 -36
  48. package/src/interfaces/seller-provider.ts +0 -81
  49. package/src/metering/index.ts +0 -6
  50. package/src/metering/receipt-generator.ts +0 -105
  51. package/src/metering/receipt-verifier.ts +0 -102
  52. package/src/metering/session-tracker.ts +0 -145
  53. package/src/metering/storage.ts +0 -600
  54. package/src/metering/token-counter.ts +0 -127
  55. package/src/metering/usage-aggregator.ts +0 -236
  56. package/src/node.ts +0 -1698
  57. package/src/p2p/connection-auth.ts +0 -152
  58. package/src/p2p/connection-manager.ts +0 -916
  59. package/src/p2p/handshake.ts +0 -162
  60. package/src/p2p/ice-config.ts +0 -59
  61. package/src/p2p/identity.ts +0 -110
  62. package/src/p2p/index.ts +0 -11
  63. package/src/p2p/keepalive.ts +0 -118
  64. package/src/p2p/message-protocol.ts +0 -171
  65. package/src/p2p/nat-traversal.ts +0 -169
  66. package/src/p2p/payment-codec.ts +0 -165
  67. package/src/p2p/payment-mux.ts +0 -153
  68. package/src/p2p/reconnect.ts +0 -117
  69. package/src/payments/balance-manager.ts +0 -77
  70. package/src/payments/buyer-payment-manager.ts +0 -414
  71. package/src/payments/disputes.ts +0 -72
  72. package/src/payments/evm/escrow-client.ts +0 -263
  73. package/src/payments/evm/keypair.ts +0 -31
  74. package/src/payments/evm/signatures.ts +0 -103
  75. package/src/payments/evm/wallet.ts +0 -42
  76. package/src/payments/index.ts +0 -50
  77. package/src/payments/settlement.ts +0 -40
  78. package/src/payments/types.ts +0 -79
  79. package/src/proxy/index.ts +0 -3
  80. package/src/proxy/provider-detection.ts +0 -78
  81. package/src/proxy/proxy-mux.ts +0 -173
  82. package/src/proxy/request-codec.ts +0 -294
  83. package/src/reputation/index.ts +0 -6
  84. package/src/reputation/rating-manager.ts +0 -118
  85. package/src/reputation/report-manager.ts +0 -91
  86. package/src/reputation/trust-engine.ts +0 -120
  87. package/src/reputation/trust-score.ts +0 -74
  88. package/src/reputation/uptime-tracker.ts +0 -155
  89. package/src/routing/default-router.ts +0 -75
  90. package/src/types/bittorrent-dht.d.ts +0 -19
  91. package/src/types/buyer.ts +0 -37
  92. package/src/types/capability.ts +0 -34
  93. package/src/types/connection.ts +0 -29
  94. package/src/types/http.ts +0 -20
  95. package/src/types/index.ts +0 -14
  96. package/src/types/metering.ts +0 -175
  97. package/src/types/nat-api.d.ts +0 -29
  98. package/src/types/peer-profile.ts +0 -25
  99. package/src/types/peer.ts +0 -62
  100. package/src/types/plugin-config.ts +0 -31
  101. package/src/types/protocol.ts +0 -162
  102. package/src/types/provider.ts +0 -40
  103. package/src/types/rating.ts +0 -23
  104. package/src/types/report.ts +0 -30
  105. package/src/types/seller.ts +0 -38
  106. package/src/types/staking.ts +0 -23
  107. package/src/utils/debug.ts +0 -30
  108. package/src/utils/hex.ts +0 -14
  109. package/tests/balance-manager.test.ts +0 -156
  110. package/tests/bootstrap.test.ts +0 -108
  111. package/tests/buyer-payment-manager.test.ts +0 -358
  112. package/tests/connection-auth.test.ts +0 -87
  113. package/tests/default-router.test.ts +0 -148
  114. package/tests/evm-keypair.test.ts +0 -173
  115. package/tests/identity.test.ts +0 -133
  116. package/tests/message-protocol.test.ts +0 -212
  117. package/tests/metadata-codec.test.ts +0 -165
  118. package/tests/metadata-validator.test.ts +0 -261
  119. package/tests/metering-storage.test.ts +0 -244
  120. package/tests/payment-codec.test.ts +0 -95
  121. package/tests/payment-mux.test.ts +0 -191
  122. package/tests/peer-selector.test.ts +0 -184
  123. package/tests/provider-detection.test.ts +0 -107
  124. package/tests/proxy-mux-security.test.ts +0 -38
  125. package/tests/receipt.test.ts +0 -215
  126. package/tests/reputation-integration.test.ts +0 -195
  127. package/tests/request-codec.test.ts +0 -144
  128. package/tests/token-counter.test.ts +0 -122
  129. package/tsconfig.json +0 -9
  130. 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
- }