agent-comms 2.31.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  4. package/dist/core/agent-registry.d.ts +1 -3
  5. package/dist/core/agent-registry.d.ts.map +1 -1
  6. package/dist/core/agent-registry.js +0 -5
  7. package/dist/core/agent-registry.js.map +1 -1
  8. package/dist/core/bridge.d.ts +0 -3
  9. package/dist/core/bridge.d.ts.map +1 -1
  10. package/dist/core/bridge.js +0 -23
  11. package/dist/core/bridge.js.map +1 -1
  12. package/dist/core/comms-store.d.ts +1 -1
  13. package/dist/core/comms-store.js +1 -1
  14. package/dist/core/delivery-engine.d.ts +1 -5
  15. package/dist/core/delivery-engine.d.ts.map +1 -1
  16. package/dist/core/delivery-engine.js +0 -12
  17. package/dist/core/delivery-engine.js.map +1 -1
  18. package/dist/core/discovery.d.ts +4 -3
  19. package/dist/core/discovery.d.ts.map +1 -1
  20. package/dist/core/discovery.js +45 -31
  21. package/dist/core/discovery.js.map +1 -1
  22. package/dist/core/index.d.ts +0 -2
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/index.js +0 -1
  25. package/dist/core/index.js.map +1 -1
  26. package/dist/core/mesh-store.d.ts +1 -15
  27. package/dist/core/mesh-store.d.ts.map +1 -1
  28. package/dist/core/mesh-store.js +2 -52
  29. package/dist/core/mesh-store.js.map +1 -1
  30. package/dist/core/room-lifecycle.d.ts +1 -4
  31. package/dist/core/room-lifecycle.d.ts.map +1 -1
  32. package/dist/core/room-lifecycle.js +0 -12
  33. package/dist/core/room-lifecycle.js.map +1 -1
  34. package/dist/core/room-messaging.d.ts +1 -3
  35. package/dist/core/room-messaging.d.ts.map +1 -1
  36. package/dist/core/room-messaging.js +0 -4
  37. package/dist/core/room-messaging.js.map +1 -1
  38. package/dist/core/room-router.d.ts.map +1 -1
  39. package/dist/core/room-router.js +2 -11
  40. package/dist/core/room-router.js.map +1 -1
  41. package/dist/core/tool.d.ts +1 -20
  42. package/dist/core/tool.d.ts.map +1 -1
  43. package/dist/core/tool.js +1 -108
  44. package/dist/core/tool.js.map +1 -1
  45. package/dist/core/types.d.ts +0 -54
  46. package/dist/core/types.d.ts.map +1 -1
  47. package/dist/core/types.js +0 -29
  48. package/dist/core/types.js.map +1 -1
  49. package/dist/core/wire-protocol.d.ts +0 -33
  50. package/dist/core/wire-protocol.d.ts.map +1 -1
  51. package/dist/core/wire-protocol.js.map +1 -1
  52. package/dist/test/agent-registry.test.js +1 -40
  53. package/dist/test/agent-registry.test.js.map +1 -1
  54. package/dist/test/delivery-engine-directed-notify.test.js +0 -39
  55. package/dist/test/delivery-engine-directed-notify.test.js.map +1 -1
  56. package/dist/test/delivery-engine.test.js +1 -18
  57. package/dist/test/delivery-engine.test.js.map +1 -1
  58. package/dist/test/mesh-store-orchestration.test.js +2 -56
  59. package/dist/test/mesh-store-orchestration.test.js.map +1 -1
  60. package/dist/test/room-lifecycle-membership.test.js +0 -30
  61. package/dist/test/room-lifecycle-membership.test.js.map +1 -1
  62. package/dist/test/room-lifecycle-remote.test.js +2 -9
  63. package/dist/test/room-lifecycle-remote.test.js.map +1 -1
  64. package/dist/test/room-lifecycle.test.js +0 -49
  65. package/dist/test/room-lifecycle.test.js.map +1 -1
  66. package/dist/test/room-messaging-durable-send.test.js +0 -1
  67. package/dist/test/room-messaging-durable-send.test.js.map +1 -1
  68. package/dist/test/room-messaging.test.js +0 -13
  69. package/dist/test/room-messaging.test.js.map +1 -1
  70. package/dist/test/visibility.integration.test.js +74 -1
  71. package/dist/test/visibility.integration.test.js.map +1 -1
  72. package/package.json +1 -1
  73. package/dist/core/federation-bridge.d.ts +0 -32
  74. package/dist/core/federation-bridge.d.ts.map +0 -1
  75. package/dist/core/federation-bridge.js +0 -112
  76. package/dist/core/federation-bridge.js.map +0 -1
  77. package/dist/core/federation.d.ts +0 -125
  78. package/dist/core/federation.d.ts.map +0 -1
  79. package/dist/core/federation.js +0 -523
  80. package/dist/core/federation.js.map +0 -1
  81. package/dist/test/federation-bridge.test.d.ts +0 -2
  82. package/dist/test/federation-bridge.test.d.ts.map +0 -1
  83. package/dist/test/federation-bridge.test.js +0 -260
  84. package/dist/test/federation-bridge.test.js.map +0 -1
  85. package/dist/test/federation.integration.test.d.ts +0 -16
  86. package/dist/test/federation.integration.test.d.ts.map +0 -1
  87. package/dist/test/federation.integration.test.js +0 -210
  88. package/dist/test/federation.integration.test.js.map +0 -1
@@ -1,125 +0,0 @@
1
- /**
2
- * FederationManager — manages TLS links between mesh coordinators.
3
- *
4
- * Federation links are persistent TLS connections between coordinators on
5
- * different machines. They forward agent presence, room memberships, and
6
- * messages for rooms marked as federated. Non-federated rooms never leave
7
- * the local mesh.
8
- *
9
- * Each link uses certificate pinning (the same trust model as TlsTransport).
10
- * The wire protocol runs over the same newline-delimited JSON framing as the
11
- * local mesh, but with `fed_*` method types.
12
- */
13
- import * as tls from "node:tls";
14
- import type { AgentIdentity, RoomMessage } from "./types.js";
15
- import type { PeerIdentity } from "./identity.js";
16
- /** Represents an active federation link (outbound or inbound). */
17
- export interface FedLink {
18
- /** Unique ID for this link (assigned locally). */
19
- id: string;
20
- /** Remote mesh ID (received during handshake). */
21
- remoteMeshId: string;
22
- /** Human-readable name for the remote mesh. */
23
- remoteName: string;
24
- /** The TLS socket. */
25
- socket: tls.TLSSocket;
26
- /** Whether the handshake has completed successfully. */
27
- ready: boolean;
28
- /** Direction of the connection. */
29
- direction: "outbound" | "inbound";
30
- }
31
- /** Callbacks the FederationManager uses to interact with local mesh state. */
32
- export interface FedCallbacks {
33
- /** Called when a remote agent becomes visible over a federation link. */
34
- onAgentVisible: (agent: AgentIdentity) => Promise<void>;
35
- /** Called when a remote agent goes offline/disappears. */
36
- onAgentGone: (agentId: string) => Promise<void>;
37
- /** Called when a message arrives for a federated room. */
38
- onRoomMessage: (roomId: string, message: RoomMessage) => Promise<void>;
39
- /** Called when a remote agent joins a federated room. */
40
- onRoomJoin: (roomId: string, agentId: string, agentName: string) => Promise<void>;
41
- /** Called when a remote agent leaves a federated room. */
42
- onRoomLeave: (roomId: string, agentId: string) => Promise<void>;
43
- /** Get all visible agents in the local mesh (for syncing to new links). */
44
- getVisibleAgents: () => AgentIdentity[];
45
- /** Get all federated rooms and their member lists (for syncing to new links). */
46
- getFederatedRoomMemberships: () => Map<string, string[]>;
47
- }
48
- export declare class FederationManager {
49
- private readonly links;
50
- private readonly identity;
51
- private readonly meshId;
52
- private readonly meshName;
53
- private readonly callbacks;
54
- private readonly pingTimers;
55
- private shutDown;
56
- private readonly pendingPongs;
57
- /**
58
- * Certificate fingerprints this instance will federate with, inbound or outbound. Empty by default — federation trusts nobody until an operator explicitly pins a remote mesh's fingerprint, the same no-CA, pin-the-key trust model ordinary peer connections already use.
59
- */
60
- private readonly trustedFingerprints;
61
- private listener;
62
- constructor(meshId: string, meshName: string, callbacks: Readonly<FedCallbacks>);
63
- /** The TLS identity used for federation connections. */
64
- get tlsIdentity(): PeerIdentity;
65
- /** Pin a remote mesh's certificate fingerprint as trusted for federation. */
66
- addTrustedFingerprint(fingerprint: string): void;
67
- /** Remove a previously pinned fingerprint. Existing links using it are not torn down. */
68
- removeTrustedFingerprint(fingerprint: string): void;
69
- /** List currently trusted fingerprints. */
70
- listTrustedFingerprints(): string[];
71
- /**
72
- * Verify the certificate a connected TLS socket presented against the trusted-fingerprint allowlist. Returns the presented fingerprint when trusted, `undefined` (and destroys the socket) otherwise.
73
- *
74
- * This is the check that was missing entirely before: the socket was accepted with `rejectUnauthorized: false` (required, since these are self-signed certs with no CA) but nothing then verified *which* self-signed cert was presented, so any certificate was accepted as a valid federation peer.
75
- */
76
- private verifyPeerOrDestroy;
77
- /**
78
- * Establish an outbound federation link to a remote coordinator.
79
- * Returns the local link ID once the handshake completes.
80
- */
81
- connect(host: string, port: number, name?: string): Promise<string>;
82
- /**
83
- * Handle an incoming TLS connection that sent a fed_handshake.
84
- * Called by the transport layer when a federation connection arrives.
85
- */
86
- handleInbound(socket: tls.TLSSocket): Promise<string>;
87
- /** Disconnect a specific federation link. */
88
- disconnect(linkId: string): Promise<void>;
89
- /** List all active federation links. */
90
- listLinks(): FedLink[];
91
- /** Broadcast an agent visibility event to all ready links. */
92
- broadcastAgentVisible(agent: AgentIdentity): Promise<void>;
93
- /** Broadcast an agent gone event to all ready links. */
94
- broadcastAgentGone(agentId: string): Promise<void>;
95
- /** Forward a room message to all ready links (federated rooms only). */
96
- forwardRoomMessage(roomId: string, message: RoomMessage): Promise<void>;
97
- /** Broadcast a room join to all ready links. */
98
- broadcastRoomJoin(roomId: string, agentId: string, agentName: string): Promise<void>;
99
- /** Broadcast a room leave to all ready links. */
100
- broadcastRoomLeave(roomId: string, agentId: string): Promise<void>;
101
- /**
102
- * Start listening for inbound federation connections. Every accepted connection is routed through `handleInbound()`, which enforces the trusted-fingerprint check before a link is ever created — nothing here bypasses that check.
103
- *
104
- * Previously nothing in the shipped product called `handleInbound()` at all: it existed only as a function the integration test invoked directly against a hand-rolled `tls.createServer`. This is that server, promoted to real code.
105
- */
106
- listen(host: string, port: number): Promise<void>;
107
- /** Stop accepting new inbound federation connections. Existing links are unaffected. */
108
- stopListening(): Promise<void>;
109
- shutdown(): Promise<void>;
110
- private tlsConnect;
111
- private wireSocket;
112
- private handleMessage;
113
- /**
114
- * After handshake, push our visible agents and federated room memberships
115
- * to the newly connected link.
116
- */
117
- private syncStateToLink;
118
- private waitForHandshake;
119
- private startPing;
120
- private sendPing;
121
- private writeToSocket;
122
- private broadcastToReady;
123
- private clearLinkTimers;
124
- }
125
- //# sourceMappingURL=federation.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../../src/core/federation.ts"],"names":[],"mappings":"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"}
@@ -1,523 +0,0 @@
1
- /**
2
- * FederationManager — manages TLS links between mesh coordinators.
3
- *
4
- * Federation links are persistent TLS connections between coordinators on
5
- * different machines. They forward agent presence, room memberships, and
6
- * messages for rooms marked as federated. Non-federated rooms never leave
7
- * the local mesh.
8
- *
9
- * Each link uses certificate pinning (the same trust model as TlsTransport).
10
- * The wire protocol runs over the same newline-delimited JSON framing as the
11
- * local mesh, but with `fed_*` method types.
12
- */
13
- import * as tls from "node:tls";
14
- import { encode, isMeshMessage, MessageBuffer } from "./wire-protocol.js";
15
- import { nanoid } from "./nanoid.js";
16
- import { fingerprintDer, generateIdentity } from "./identity.js";
17
- // ---------------------------------------------------------------------------
18
- // Constants
19
- // ---------------------------------------------------------------------------
20
- const FED_PING_INTERVAL_MS = 30_000;
21
- const FED_PING_TIMEOUT_MS = 10_000;
22
- const FED_VERSION = "1.0.0";
23
- /** Length (in bytes of entropy) of a locally-assigned federation link ID. */
24
- const FED_LINK_ID_LENGTH = 8;
25
- /** How long to wait for the remote side to complete the fed_handshake/fed_ack exchange before giving up. */
26
- const FED_HANDSHAKE_TIMEOUT_MS = 5000;
27
- /** How long to wait for the underlying TLS socket itself to connect before giving up. */
28
- const FED_CONNECT_TIMEOUT_MS = 5000;
29
- /** How often {@link FederationManager.waitForHandshake} polls link readiness. */
30
- const FED_HANDSHAKE_POLL_INTERVAL_MS = 50;
31
- // ---------------------------------------------------------------------------
32
- // FederationManager
33
- // ---------------------------------------------------------------------------
34
- export class FederationManager {
35
- links = new Map();
36
- identity;
37
- meshId;
38
- meshName;
39
- callbacks;
40
- pingTimers = new Map();
41
- shutDown = false;
42
- pendingPongs = new Map();
43
- /**
44
- * Certificate fingerprints this instance will federate with, inbound or outbound. Empty by default — federation trusts nobody until an operator explicitly pins a remote mesh's fingerprint, the same no-CA, pin-the-key trust model ordinary peer connections already use.
45
- */
46
- trustedFingerprints = new Set();
47
- listener;
48
- constructor(meshId, meshName, callbacks) {
49
- this.meshId = meshId;
50
- this.meshName = meshName;
51
- this.callbacks = callbacks;
52
- this.identity = generateIdentity();
53
- }
54
- /** The TLS identity used for federation connections. */
55
- get tlsIdentity() {
56
- return this.identity;
57
- }
58
- // -----------------------------------------------------------------------
59
- // Trust — which remote mesh fingerprints this instance will federate with
60
- // -----------------------------------------------------------------------
61
- /** Pin a remote mesh's certificate fingerprint as trusted for federation. */
62
- addTrustedFingerprint(fingerprint) {
63
- this.trustedFingerprints.add(fingerprint);
64
- }
65
- /** Remove a previously pinned fingerprint. Existing links using it are not torn down. */
66
- removeTrustedFingerprint(fingerprint) {
67
- this.trustedFingerprints.delete(fingerprint);
68
- }
69
- /** List currently trusted fingerprints. */
70
- listTrustedFingerprints() {
71
- return [...this.trustedFingerprints];
72
- }
73
- /**
74
- * Verify the certificate a connected TLS socket presented against the trusted-fingerprint allowlist. Returns the presented fingerprint when trusted, `undefined` (and destroys the socket) otherwise.
75
- *
76
- * This is the check that was missing entirely before: the socket was accepted with `rejectUnauthorized: false` (required, since these are self-signed certs with no CA) but nothing then verified *which* self-signed cert was presented, so any certificate was accepted as a valid federation peer.
77
- */
78
- verifyPeerOrDestroy(socket) {
79
- const cert = socket.getPeerCertificate();
80
- // Node's types declare every PeerCertificate field non-optional, but the documented runtime behaviour when the peer presents no certificate at all is an empty object — not null/undefined, and not a Buffer-typed `raw`. Detect that real shape rather than trusting the declared type.
81
- if (Object.keys(cert).length === 0) {
82
- socket.destroy();
83
- return undefined;
84
- }
85
- const fingerprint = fingerprintDer(cert.raw);
86
- if (!this.trustedFingerprints.has(fingerprint)) {
87
- socket.destroy();
88
- return undefined;
89
- }
90
- return fingerprint;
91
- }
92
- // -----------------------------------------------------------------------
93
- // Outbound links
94
- // -----------------------------------------------------------------------
95
- /**
96
- * Establish an outbound federation link to a remote coordinator.
97
- * Returns the local link ID once the handshake completes.
98
- */
99
- async connect(host, port, name) {
100
- if (this.shutDown)
101
- throw new Error("FederationManager is shut down");
102
- const linkId = nanoid(FED_LINK_ID_LENGTH);
103
- const socket = await this.tlsConnect(host, port);
104
- if (this.verifyPeerOrDestroy(socket) === undefined) {
105
- throw new Error(`Federation connection to ${host}:${String(port)} rejected: ` +
106
- "the presented certificate is not in the trusted-fingerprint allowlist. " +
107
- "Call addTrustedFingerprint() with the remote mesh's fingerprint first.");
108
- }
109
- const link = {
110
- id: linkId,
111
- remoteMeshId: "",
112
- remoteName: name ?? `${host}:${String(port)}`,
113
- socket,
114
- ready: false,
115
- direction: "outbound",
116
- };
117
- this.links.set(linkId, link);
118
- // Send handshake
119
- const handshake = {
120
- method: "fed_handshake",
121
- meshId: this.meshId,
122
- name: this.meshName,
123
- version: FED_VERSION,
124
- };
125
- await this.writeToSocket(socket, handshake);
126
- // Wire up incoming message handling
127
- this.wireSocket(linkId, socket);
128
- // Wait for fed_ack (with timeout)
129
- await this.waitForHandshake(linkId, FED_HANDSHAKE_TIMEOUT_MS);
130
- return linkId;
131
- }
132
- // -----------------------------------------------------------------------
133
- // Inbound links (server)
134
- // -----------------------------------------------------------------------
135
- /**
136
- * Handle an incoming TLS connection that sent a fed_handshake.
137
- * Called by the transport layer when a federation connection arrives.
138
- */
139
- async handleInbound(socket) {
140
- if (this.shutDown) {
141
- socket.destroy();
142
- throw new Error("FederationManager is shut down");
143
- }
144
- if (this.verifyPeerOrDestroy(socket) === undefined) {
145
- throw new Error("Inbound federation connection rejected: the presented certificate " +
146
- "is not in the trusted-fingerprint allowlist.");
147
- }
148
- const linkId = nanoid(FED_LINK_ID_LENGTH);
149
- const link = {
150
- id: linkId,
151
- remoteMeshId: "",
152
- remoteName: "unknown",
153
- socket,
154
- ready: false,
155
- direction: "inbound",
156
- };
157
- this.links.set(linkId, link);
158
- this.wireSocket(linkId, socket);
159
- // Wait for the remote side's handshake
160
- await this.waitForHandshake(linkId, FED_HANDSHAKE_TIMEOUT_MS);
161
- return linkId;
162
- }
163
- // -----------------------------------------------------------------------
164
- // Link management
165
- // -----------------------------------------------------------------------
166
- /** Disconnect a specific federation link. */
167
- async disconnect(linkId) {
168
- const link = this.links.get(linkId);
169
- if (!link)
170
- return Promise.resolve();
171
- this.clearLinkTimers(linkId);
172
- link.ready = false;
173
- link.socket.unref();
174
- link.socket.destroy();
175
- this.links.delete(linkId);
176
- return Promise.resolve();
177
- }
178
- /** List all active federation links. */
179
- listLinks() {
180
- return [...this.links.values()].filter((l) => l.ready);
181
- }
182
- // -----------------------------------------------------------------------
183
- // Broadcasting to federated links
184
- // -----------------------------------------------------------------------
185
- /** Broadcast an agent visibility event to all ready links. */
186
- async broadcastAgentVisible(agent) {
187
- const msg = { method: "fed_agent_visible", agent };
188
- await this.broadcastToReady(msg);
189
- }
190
- /** Broadcast an agent gone event to all ready links. */
191
- async broadcastAgentGone(agentId) {
192
- const msg = { method: "fed_agent_gone", agentId };
193
- await this.broadcastToReady(msg);
194
- }
195
- /** Forward a room message to all ready links (federated rooms only). */
196
- async forwardRoomMessage(roomId, message) {
197
- const msg = { method: "fed_room_message", roomId, message };
198
- await this.broadcastToReady(msg);
199
- }
200
- /** Broadcast a room join to all ready links. */
201
- async broadcastRoomJoin(roomId, agentId, agentName) {
202
- const msg = {
203
- method: "fed_room_join",
204
- roomId,
205
- agentId,
206
- agentName,
207
- };
208
- await this.broadcastToReady(msg);
209
- }
210
- /** Broadcast a room leave to all ready links. */
211
- async broadcastRoomLeave(roomId, agentId) {
212
- const msg = { method: "fed_room_leave", roomId, agentId };
213
- await this.broadcastToReady(msg);
214
- }
215
- // -----------------------------------------------------------------------
216
- // Inbound listener — accepts federation links from remote coordinators
217
- // -----------------------------------------------------------------------
218
- /**
219
- * Start listening for inbound federation connections. Every accepted connection is routed through `handleInbound()`, which enforces the trusted-fingerprint check before a link is ever created — nothing here bypasses that check.
220
- *
221
- * Previously nothing in the shipped product called `handleInbound()` at all: it existed only as a function the integration test invoked directly against a hand-rolled `tls.createServer`. This is that server, promoted to real code.
222
- */
223
- async listen(host, port) {
224
- if (this.listener) {
225
- throw new Error("FederationManager is already listening");
226
- }
227
- return new Promise((resolve, reject) => {
228
- const server = tls.createServer({
229
- key: this.identity.privateKey,
230
- cert: this.identity.certificate,
231
- // Same as the outbound side: no CA, so we don't ask Node to verify the chain. requestCert is what makes the connecting peer's own certificate available to verifyPeerOrDestroy() inside handleInbound() — without it there is nothing to check.
232
- rejectUnauthorized: false,
233
- requestCert: true,
234
- }, (socket) => {
235
- this.handleInbound(socket).catch(() => {
236
- // Rejected (untrusted fingerprint, or shutting down) — the socket is already destroyed inside handleInbound/verifyPeerOrDestroy.
237
- });
238
- });
239
- server.listen(port, host, () => {
240
- this.listener = server;
241
- resolve();
242
- });
243
- server.on("error", reject);
244
- });
245
- }
246
- /** Stop accepting new inbound federation connections. Existing links are unaffected. */
247
- async stopListening() {
248
- const server = this.listener;
249
- if (!server)
250
- return Promise.resolve();
251
- this.listener = undefined;
252
- return new Promise((resolve) => {
253
- server.close(() => {
254
- resolve();
255
- });
256
- });
257
- }
258
- // -----------------------------------------------------------------------
259
- // Shutdown
260
- // -----------------------------------------------------------------------
261
- async shutdown() {
262
- this.shutDown = true;
263
- await this.stopListening();
264
- for (const linkId of [...this.links.keys()]) {
265
- await this.disconnect(linkId);
266
- }
267
- }
268
- // -----------------------------------------------------------------------
269
- // Internal — TLS connection
270
- // -----------------------------------------------------------------------
271
- async tlsConnect(host, port) {
272
- return new Promise((resolve, reject) => {
273
- const socket = tls.connect({
274
- key: this.identity.privateKey,
275
- cert: this.identity.certificate,
276
- host,
277
- port,
278
- rejectUnauthorized: false,
279
- requestCert: true,
280
- }, () => {
281
- resolve(socket);
282
- });
283
- const timer = setTimeout(() => {
284
- socket.destroy();
285
- reject(new Error(`Federation connection timeout to ${host}:${String(port)}`));
286
- }, FED_CONNECT_TIMEOUT_MS);
287
- socket.on("error", (err) => {
288
- clearTimeout(timer);
289
- reject(err);
290
- });
291
- });
292
- }
293
- // -----------------------------------------------------------------------
294
- // Internal — socket wiring
295
- // -----------------------------------------------------------------------
296
- wireSocket(linkId, socket) {
297
- const buffer = new MessageBuffer();
298
- socket.on("data", (data) => {
299
- const items = buffer.append(data.toString());
300
- for (const item of items) {
301
- if (isMeshMessage(item)) {
302
- void this.handleMessage(linkId, item);
303
- }
304
- }
305
- });
306
- socket.on("error", () => {
307
- void this.disconnect(linkId);
308
- });
309
- socket.on("close", () => {
310
- void this.disconnect(linkId);
311
- });
312
- }
313
- // -----------------------------------------------------------------------
314
- // Internal — message handling
315
- // -----------------------------------------------------------------------
316
- async handleMessage(linkId, msg) {
317
- const link = this.links.get(linkId);
318
- if (!link || this.shutDown)
319
- return;
320
- switch (msg.method) {
321
- case "fed_handshake": {
322
- // Inbound connection sending its handshake
323
- link.remoteMeshId = msg.meshId;
324
- link.remoteName = msg.name;
325
- link.ready = true;
326
- // Respond with ack
327
- const ack = {
328
- method: "fed_ack",
329
- meshId: this.meshId,
330
- name: this.meshName,
331
- version: FED_VERSION,
332
- };
333
- await this.writeToSocket(link.socket, ack);
334
- // Sync local state to the new link
335
- await this.syncStateToLink(linkId);
336
- // Start ping for this link
337
- this.startPing(linkId);
338
- break;
339
- }
340
- case "fed_ack": {
341
- link.remoteMeshId = msg.meshId;
342
- link.remoteName = msg.name;
343
- link.ready = true;
344
- // Sync local state to the new link
345
- await this.syncStateToLink(linkId);
346
- // Start ping for this link
347
- this.startPing(linkId);
348
- break;
349
- }
350
- case "fed_agent_visible": {
351
- await this.callbacks.onAgentVisible(msg.agent);
352
- break;
353
- }
354
- case "fed_agent_gone": {
355
- await this.callbacks.onAgentGone(msg.agentId);
356
- break;
357
- }
358
- case "fed_room_message": {
359
- await this.callbacks.onRoomMessage(msg.roomId, msg.message);
360
- break;
361
- }
362
- case "fed_room_join": {
363
- await this.callbacks.onRoomJoin(msg.roomId, msg.agentId, msg.agentName);
364
- break;
365
- }
366
- case "fed_room_leave": {
367
- await this.callbacks.onRoomLeave(msg.roomId, msg.agentId);
368
- break;
369
- }
370
- case "fed_ping": {
371
- const pong = { method: "fed_pong" };
372
- await this.writeToSocket(link.socket, pong);
373
- break;
374
- }
375
- case "fed_pong": {
376
- const pending = this.pendingPongs.get(linkId);
377
- if (pending) {
378
- clearTimeout(pending);
379
- this.pendingPongs.delete(linkId);
380
- }
381
- break;
382
- }
383
- case "state_sync":
384
- case "state_update":
385
- case "introduce":
386
- case "connect_request":
387
- case "peer_list":
388
- case "peer_joined":
389
- case "peer_left":
390
- case "become_coordinator": {
391
- // Local-mesh-only message kinds — never sent over a federation link; ignore rather than throw so an unexpected wire message can't tear down the link.
392
- break;
393
- }
394
- }
395
- }
396
- // -----------------------------------------------------------------------
397
- // Internal — state sync
398
- // -----------------------------------------------------------------------
399
- /**
400
- * After handshake, push our visible agents and federated room memberships
401
- * to the newly connected link.
402
- */
403
- async syncStateToLink(linkId) {
404
- const link = this.links.get(linkId);
405
- if (link?.ready !== true)
406
- return;
407
- // Sync visible agents
408
- const agents = this.callbacks.getVisibleAgents();
409
- for (const agent of agents) {
410
- const msg = { method: "fed_agent_visible", agent };
411
- await this.writeToSocket(link.socket, msg);
412
- }
413
- // Sync federated room memberships
414
- const rooms = this.callbacks.getFederatedRoomMemberships();
415
- for (const [roomId, memberIds] of rooms) {
416
- for (const memberId of memberIds) {
417
- const agent = this.callbacks
418
- .getVisibleAgents()
419
- .find((a) => a.id === memberId);
420
- const agentName = agent?.name ?? memberId;
421
- const msg = {
422
- method: "fed_room_join",
423
- roomId,
424
- agentId: memberId,
425
- agentName,
426
- };
427
- await this.writeToSocket(link.socket, msg);
428
- }
429
- }
430
- }
431
- // -----------------------------------------------------------------------
432
- // Internal — handshake waiting
433
- // -----------------------------------------------------------------------
434
- async waitForHandshake(linkId, timeoutMs) {
435
- return new Promise((resolve, reject) => {
436
- const startTime = Date.now();
437
- const check = () => {
438
- const link = this.links.get(linkId);
439
- if (link?.ready === true) {
440
- resolve();
441
- return;
442
- }
443
- if (Date.now() - startTime > timeoutMs) {
444
- void this.disconnect(linkId);
445
- reject(new Error(`Federation handshake timeout for link ${linkId}`));
446
- return;
447
- }
448
- setTimeout(check, FED_HANDSHAKE_POLL_INTERVAL_MS);
449
- };
450
- check();
451
- });
452
- }
453
- // -----------------------------------------------------------------------
454
- // Internal — ping/pong health monitoring
455
- // -----------------------------------------------------------------------
456
- startPing(linkId) {
457
- if (this.pingTimers.has(linkId))
458
- return;
459
- const timer = setInterval(() => {
460
- void this.sendPing(linkId);
461
- }, FED_PING_INTERVAL_MS);
462
- this.pingTimers.set(linkId, timer);
463
- }
464
- async sendPing(linkId) {
465
- const link = this.links.get(linkId);
466
- if (link?.ready !== true)
467
- return;
468
- const msg = { method: "fed_ping" };
469
- await this.writeToSocket(link.socket, msg);
470
- // Set pong timeout
471
- const timer = setTimeout(() => {
472
- // No pong received — disconnect the link
473
- void this.disconnect(linkId);
474
- }, FED_PING_TIMEOUT_MS);
475
- this.pendingPongs.set(linkId, timer);
476
- }
477
- // -----------------------------------------------------------------------
478
- // Internal — helpers
479
- // -----------------------------------------------------------------------
480
- async writeToSocket(socket, msg) {
481
- if (socket.destroyed)
482
- return;
483
- const data = encode(msg);
484
- await new Promise((resolve, reject) => {
485
- socket.write(data, "utf-8", (err) => {
486
- if (err)
487
- reject(err);
488
- else
489
- resolve();
490
- });
491
- });
492
- }
493
- async broadcastToReady(msg) {
494
- const data = encode(msg);
495
- const writes = [];
496
- for (const [, link] of this.links) {
497
- if (!link.ready || link.socket.destroyed)
498
- continue;
499
- writes.push(new Promise((resolve) => {
500
- link.socket.write(data, "utf-8", (err) => {
501
- if (err) {
502
- void this.disconnect(link.id);
503
- }
504
- resolve();
505
- });
506
- }));
507
- }
508
- await Promise.all(writes);
509
- }
510
- clearLinkTimers(linkId) {
511
- const pingTimer = this.pingTimers.get(linkId);
512
- if (pingTimer !== undefined) {
513
- clearInterval(pingTimer);
514
- this.pingTimers.delete(linkId);
515
- }
516
- const pongTimer = this.pendingPongs.get(linkId);
517
- if (pongTimer !== undefined) {
518
- clearTimeout(pongTimer);
519
- this.pendingPongs.delete(linkId);
520
- }
521
- }
522
- }
523
- //# sourceMappingURL=federation.js.map