agent-comms 8.15.8 → 8.15.10

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 (184) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/bridges/user/web/server.d.ts.map +1 -1
  4. package/dist/bridges/user/web/server.js +44 -24
  5. package/dist/bridges/user/web/server.js.map +1 -1
  6. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
  7. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
  8. package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
  9. package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
  10. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
  11. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
  12. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
  13. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
  14. package/dist/bridges/user/web/web-assets.d.ts +28 -0
  15. package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
  16. package/dist/bridges/user/web/web-assets.js +67 -0
  17. package/dist/bridges/user/web/web-assets.js.map +1 -0
  18. package/package.json +8 -2
  19. package/src/account-cli.ts +195 -0
  20. package/src/account-join-cli.ts +191 -0
  21. package/src/bridges/cc-peer/approval-commands.ts +82 -0
  22. package/src/bridges/cc-peer/bridge.ts +138 -0
  23. package/src/bridges/cc-peer/default-front.ts +58 -0
  24. package/src/bridges/cc-peer/front-controller.ts +160 -0
  25. package/src/bridges/cc-peer/front-relay.ts +191 -0
  26. package/src/bridges/cc-peer/front-runtime.ts +167 -0
  27. package/src/bridges/cc-peer/front.ts +59 -0
  28. package/src/bridges/cc-peer/reply-aliases.ts +90 -0
  29. package/src/bridges/cc-peer/run.ts +100 -0
  30. package/src/bridges/cc-peer/shared-peer.ts +28 -0
  31. package/src/bridges/claude-code/channel.ts +318 -0
  32. package/src/bridges/codex/tool.ts +132 -0
  33. package/src/bridges/mcp/server.ts +137 -0
  34. package/src/bridges/opencode/plugin.ts +130 -0
  35. package/src/bridges/pi/extension.ts +330 -0
  36. package/src/bridges/registry.ts +17 -0
  37. package/src/bridges/user/cli.ts +87 -0
  38. package/src/bridges/user/controller.ts +345 -0
  39. package/src/bridges/user/tui.ts +300 -0
  40. package/src/bridges/user/web/actions.ts +173 -0
  41. package/src/bridges/user/web/contract.ts +222 -0
  42. package/src/bridges/user/web/dist/bundle.js +49 -0
  43. package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
  44. package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
  45. package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
  46. package/src/bridges/user/web/dist/index.html +21 -0
  47. package/src/bridges/user/web/dist/manifest.json +26 -0
  48. package/src/bridges/user/web/dist/mesh-worker.js +64 -0
  49. package/src/bridges/user/web/dist/sw.js +1 -0
  50. package/src/bridges/user/web/event-publisher.ts +114 -0
  51. package/src/bridges/user/web/port-discovery.ts +49 -0
  52. package/src/bridges/user/web/reads.ts +35 -0
  53. package/src/bridges/user/web/router.ts +158 -0
  54. package/src/bridges/user/web/server.ts +782 -0
  55. package/src/bridges/user/web/vite.config.ts +71 -0
  56. package/src/bridges/user/web/web-access.ts +404 -0
  57. package/src/bridges/user/web/web-assets.ts +90 -0
  58. package/src/bridges/user/web/web-beacon.ts +69 -0
  59. package/src/bridges/user/web/web-console-static.ts +90 -0
  60. package/src/cli-main.ts +635 -0
  61. package/src/cli.ts +13 -0
  62. package/src/core/account-bundle.ts +264 -0
  63. package/src/core/account-join.ts +289 -0
  64. package/src/core/account-ledger-crypto.ts +154 -0
  65. package/src/core/account-ledger-store.ts +66 -0
  66. package/src/core/account-ledger.ts +385 -0
  67. package/src/core/agent-details.ts +216 -0
  68. package/src/core/agent-list-format.ts +97 -0
  69. package/src/core/agent-registry.ts +282 -0
  70. package/src/core/agent-table.ts +58 -0
  71. package/src/core/atomic-file.ts +46 -0
  72. package/src/core/bind-retry.ts +38 -0
  73. package/src/core/bridge-mesh.ts +176 -0
  74. package/src/core/bridge.ts +718 -0
  75. package/src/core/capability-ask.ts +162 -0
  76. package/src/core/capability-bubble-up.ts +237 -0
  77. package/src/core/comms-store.ts +130 -0
  78. package/src/core/connection-approval.ts +134 -0
  79. package/src/core/connection-code-tool.ts +92 -0
  80. package/src/core/connection-code.ts +296 -0
  81. package/src/core/coordinator-role.ts +224 -0
  82. package/src/core/data-directory.ts +32 -0
  83. package/src/core/data-frame-routing.ts +309 -0
  84. package/src/core/default-command.ts +12 -0
  85. package/src/core/delegation-policy.ts +41 -0
  86. package/src/core/delivery-engine.ts +649 -0
  87. package/src/core/device-membership-verification.ts +125 -0
  88. package/src/core/device-membership.ts +130 -0
  89. package/src/core/directory-admission.ts +155 -0
  90. package/src/core/discovery-mdns.ts +247 -0
  91. package/src/core/discovery-tailscale.ts +224 -0
  92. package/src/core/discovery.ts +303 -0
  93. package/src/core/display-name.ts +73 -0
  94. package/src/core/dm-grant-actions.ts +72 -0
  95. package/src/core/dm-send-delegation.ts +73 -0
  96. package/src/core/dm-token-verification.ts +76 -0
  97. package/src/core/election-sessions.ts +135 -0
  98. package/src/core/error-reporting.ts +85 -0
  99. package/src/core/first-contact.ts +242 -0
  100. package/src/core/gateway-trust-actions.ts +184 -0
  101. package/src/core/gateway-trust.ts +305 -0
  102. package/src/core/gossip-directory.ts +276 -0
  103. package/src/core/gossip-extensions.ts +123 -0
  104. package/src/core/group-proofs.ts +205 -0
  105. package/src/core/handshake.ts +354 -0
  106. package/src/core/hub-forwarding.ts +24 -0
  107. package/src/core/hub-link.ts +176 -0
  108. package/src/core/hub-session.ts +374 -0
  109. package/src/core/identity-store.ts +715 -0
  110. package/src/core/identity.ts +489 -0
  111. package/src/core/index.ts +65 -0
  112. package/src/core/issuer-grouping.ts +165 -0
  113. package/src/core/issuer-identity-file.ts +187 -0
  114. package/src/core/lan-web-actions.ts +34 -0
  115. package/src/core/lan-web-uis.ts +55 -0
  116. package/src/core/ledger-lock.ts +81 -0
  117. package/src/core/listener-registry.ts +133 -0
  118. package/src/core/loopback-address.ts +22 -0
  119. package/src/core/machine-identity.ts +77 -0
  120. package/src/core/machine-list-groups.ts +53 -0
  121. package/src/core/mcp-registry-retry.ts +69 -0
  122. package/src/core/mcp-registry-version.ts +18 -0
  123. package/src/core/membership-proof.ts +139 -0
  124. package/src/core/membership-proofs.ts +149 -0
  125. package/src/core/mesh-graph-trace-tool.ts +81 -0
  126. package/src/core/mesh-graph.ts +89 -0
  127. package/src/core/mesh-network-actions.ts +183 -0
  128. package/src/core/mesh-store-options.ts +23 -0
  129. package/src/core/mesh-store-shared.ts +134 -0
  130. package/src/core/mesh-store.ts +1114 -0
  131. package/src/core/name-claim.ts +145 -0
  132. package/src/core/naming-actions.ts +90 -0
  133. package/src/core/naming.ts +138 -0
  134. package/src/core/nanoid.ts +20 -0
  135. package/src/core/network-interfaces.ts +24 -0
  136. package/src/core/package-version.ts +34 -0
  137. package/src/core/peer-lifecycle.ts +377 -0
  138. package/src/core/peer-link-error.ts +41 -0
  139. package/src/core/peer-versions.ts +98 -0
  140. package/src/core/pending-connection.ts +53 -0
  141. package/src/core/petnames.ts +121 -0
  142. package/src/core/pgp-keyserver.ts +32 -0
  143. package/src/core/pid-lock-file.ts +122 -0
  144. package/src/core/push-manager.ts +76 -0
  145. package/src/core/random-id.ts +9 -0
  146. package/src/core/receiver-set.ts +42 -0
  147. package/src/core/relay-role.ts +182 -0
  148. package/src/core/relay-selection.ts +120 -0
  149. package/src/core/relay-server.ts +198 -0
  150. package/src/core/relay-uplink.ts +78 -0
  151. package/src/core/request-timeouts.ts +37 -0
  152. package/src/core/room-discovery.ts +48 -0
  153. package/src/core/room-lifecycle.ts +838 -0
  154. package/src/core/room-lookup.ts +41 -0
  155. package/src/core/room-messaging.ts +247 -0
  156. package/src/core/room-notice-log.ts +61 -0
  157. package/src/core/room-path.ts +84 -0
  158. package/src/core/room-protocol.ts +893 -0
  159. package/src/core/room-router.ts +180 -0
  160. package/src/core/room-token-verification.ts +85 -0
  161. package/src/core/room-wire-extensions.ts +60 -0
  162. package/src/core/self-name-claim.ts +67 -0
  163. package/src/core/send-outcome.ts +69 -0
  164. package/src/core/shutdown-signals.ts +57 -0
  165. package/src/core/stale-agent-checker.ts +108 -0
  166. package/src/core/store.ts +704 -0
  167. package/src/core/token-text.ts +40 -0
  168. package/src/core/tool.ts +853 -0
  169. package/src/core/transport-events.ts +73 -0
  170. package/src/core/transport-queries.ts +54 -0
  171. package/src/core/transport.ts +367 -0
  172. package/src/core/types.ts +550 -0
  173. package/src/core/user-identity.ts +290 -0
  174. package/src/core/vapid.ts +81 -0
  175. package/src/core/version-check.ts +147 -0
  176. package/src/core/version-report-actions.ts +79 -0
  177. package/src/core/web-beacon.ts +31 -0
  178. package/src/core/web-push.ts +322 -0
  179. package/src/core/wire-mesh-core-version.ts +25 -0
  180. package/src/core/wire-mesh-identity.ts +36 -0
  181. package/src/core/wire-mesh-transport-options.ts +82 -0
  182. package/src/core/wire-mesh-transport.ts +1007 -0
  183. package/src/core/wire-protocol.ts +153 -0
  184. package/src/core/ws-dial.ts +191 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Wire protocol — framing, encoding, and message types for the TCP mesh.
3
+ *
4
+ * Transport-agnostic: carries the protocol contract between peers without depending on net.Socket or any specific transport implementation. Any MeshTransport implementation (WireMeshTransport is the one production transport today) uses these types.
5
+ */
6
+
7
+ import type {
8
+ AgentIdentity,
9
+ DeliveryEvent,
10
+ DmMessage,
11
+ Room,
12
+ RoomMessage,
13
+ } from "./types.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Peer info
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface PeerInfo {
20
+ id: string;
21
+ port: number;
22
+ startedAt: string;
23
+ /** The host this peer's data port listens on. Absent means the machine-local default (localhost): every peer a coordinator introduction or a same-machine first contact names needs no host, while a peer discovered on another machine (first-contact beacons, agent-comms#341) carries the address its beacon came from. */
24
+ host?: string;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Serialised mesh state
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface SerialisedState {
32
+ agents: Record<string, AgentIdentity>;
33
+ rooms: Record<string, Room>;
34
+ messages: Record<string, RoomMessage[]>;
35
+ dms: Record<string, DmMessage[]>;
36
+ /**
37
+ * Pending delivery events per target agent, replicated on every peer. A returning peer replays its own queue from the first snapshot it receives, so events pushed while its process was down still fire onDelivery (#28).
38
+ */
39
+ deliveryQueues: Record<string, DeliveryEvent[]>;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // State patches (incremental updates)
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export type MeshStatePatch =
47
+ | { type: "agent_upsert"; agent: AgentIdentity }
48
+ | { type: "agent_offline"; agentId: string }
49
+ | { type: "room_upsert"; room: Room }
50
+ | { type: "room_delete"; roomId: string }
51
+ | { type: "message_add"; roomId: string; message: RoomMessage }
52
+ | { type: "dm_add"; key: string; message: DmMessage }
53
+ | { type: "delivery"; agentId: string; event: DeliveryEvent };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Wire message union
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export type MeshMessage =
60
+ | { method: "state_sync"; state: SerialisedState }
61
+ | { method: "state_update"; patch: MeshStatePatch }
62
+ | { method: "introduce"; peerId: string; dataPort: number }
63
+ | {
64
+ method: "connect_request";
65
+ peerId: string;
66
+ dataPort: number;
67
+ name: string;
68
+ fingerprint: string;
69
+ }
70
+ | { method: "peer_list"; peers: PeerInfo[] }
71
+ | { method: "peer_joined"; peer: PeerInfo }
72
+ | { method: "peer_left"; peerId: string }
73
+ | { method: "become_coordinator"; peerList: PeerInfo[] };
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Framing — newline-delimited JSON
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export function encode(msg: MeshMessage): string {
80
+ return JSON.stringify(msg) + "\n";
81
+ }
82
+
83
+ export class MessageBuffer {
84
+ private buffer = "";
85
+
86
+ append(data: string): unknown[] {
87
+ this.buffer += data;
88
+ const results: unknown[] = [];
89
+ let idx = this.buffer.indexOf("\n");
90
+ while (idx !== -1) {
91
+ const line = this.buffer.slice(0, idx);
92
+ this.buffer = this.buffer.slice(idx + 1);
93
+ if (line.length > 0) {
94
+ try {
95
+ results.push(JSON.parse(line));
96
+ } catch {
97
+ /* skip malformed lines */
98
+ }
99
+ }
100
+ idx = this.buffer.indexOf("\n");
101
+ }
102
+ return results;
103
+ }
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Type guard
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export function isMeshMessage(value: unknown): value is MeshMessage {
111
+ if (typeof value !== "object" || value === null) return false;
112
+ if (!("method" in value)) return false;
113
+ return typeof value.method === "string";
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Wire evolution tolerance (#31 direction 2)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * The shape a state_sync payload actually has on the wire: `SerialisedState` with every field that predates #29/#30 -- and each entity's `version` within those fields -- optional, since nothing validates this JSON.parse'd message beyond `isMeshMessage`'s bare `method` check. `SerialisedState` itself stays the fully-populated type domain logic works with elsewhere; this is deliberately narrower than `Partial<SerialisedState>` so it models only the specific absences an older build can actually produce.
122
+ */
123
+ export interface WireStateInput {
124
+ agents?: Record<
125
+ string,
126
+ Omit<AgentIdentity, "version"> & { version?: number }
127
+ >;
128
+ rooms?: Record<string, Omit<Room, "version"> & { version?: number }>;
129
+ messages?: SerialisedState["messages"];
130
+ dms?: SerialisedState["dms"];
131
+ deliveryQueues?: SerialisedState["deliveryQueues"];
132
+ }
133
+
134
+ /**
135
+ * Normalises a state_sync payload at the wire boundary so a snapshot from an older build — one predating the entity `version` fields (#29) or `deliveryQueues` (#30) — parses to a complete state instead of throwing in `applyStateSync`. Missing collections default to empty; missing entity versions default to 1 (a pre-versioning sender's records are treated as fresh, which is what they were when written — there was no older format). The handshake (handshake.ts) is the loud, forward-looking half of the #31 fix; this is the tolerant half for peers that never negotiate.
136
+ */
137
+ export function normaliseWireState(state: WireStateInput): SerialisedState {
138
+ const agents: Record<string, AgentIdentity> = {};
139
+ for (const [id, agent] of Object.entries(state.agents ?? {})) {
140
+ agents[id] = { ...agent, version: agent.version ?? 1 };
141
+ }
142
+ const rooms: Record<string, Room> = {};
143
+ for (const [id, room] of Object.entries(state.rooms ?? {})) {
144
+ rooms[id] = { ...room, version: room.version ?? 1 };
145
+ }
146
+ return {
147
+ agents,
148
+ rooms,
149
+ messages: state.messages ?? {},
150
+ dms: state.dms ?? {},
151
+ deliveryQueues: state.deliveryQueues ?? {},
152
+ };
153
+ }
@@ -0,0 +1,191 @@
1
+ // A ws-library dial adapter for outbound remote connections to a WebSocket-served hub (e.g. the mesh.exadev.io cloudflare-hub, which answers wss:// only): one CBOR frame per binary WebSocket message, the identical framing wire-mesh-node's own WebSocket transport uses, so a hub sees the same bytes from this client as from any other. Agent-comms' own edge adapter rather than a wire-mesh-node dependency because wire-mesh-node is not published to npm as a consumable package (a placeholder 0.0.0), while the dial is genuinely this app's concern at its own edge -- the same layering its TLS transport adapter already occupies.
2
+
3
+ import { WebSocket as WsSocket } from "ws";
4
+ import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
5
+ import { frameSchema, type Frame } from "wire-mesh-core/generated/protocol";
6
+ import type { Connection, Transport } from "wire-mesh-core/ports/transport";
7
+
8
+ const CONNECT_TIMEOUT_MS = 10_000;
9
+ // RFC 6455 close codes, named rather than bare: 1000 normal closure, 1002 protocol error.
10
+ const CLOSE_NORMAL = 1000;
11
+ const CLOSE_PROTOCOL_ERROR = 1002;
12
+
13
+ /** Rejects addresses that are neither ws:// nor wss:// URLs -- a wrong-scheme dial would otherwise surface as an opaque socket error. */
14
+ function assertWsUrl(url: string): void {
15
+ if (!/^wss?:\/\/./.test(url)) {
16
+ throw new Error(`expected a ws:// or wss:// URL, got "${url}"`);
17
+ }
18
+ }
19
+
20
+ export async function connectWsUrl(url: string): Promise<Connection> {
21
+ assertWsUrl(url);
22
+ return new Promise<Connection>((resolve, reject) => {
23
+ const socket = new WsSocket(url);
24
+ socket.binaryType = "arraybuffer";
25
+ const timer = setTimeout(() => {
26
+ socket.terminate();
27
+ reject(
28
+ new Error(
29
+ `ws dial timed out after ${String(CONNECT_TIMEOUT_MS)}ms: ${url}`,
30
+ ),
31
+ );
32
+ }, CONNECT_TIMEOUT_MS);
33
+ socket.once("open", () => {
34
+ clearTimeout(timer);
35
+ resolve(wrapWsSocket(socket));
36
+ });
37
+ socket.once("error", (error) => {
38
+ clearTimeout(timer);
39
+ reject(error instanceof Error ? error : new Error(String(error)));
40
+ });
41
+ });
42
+ }
43
+
44
+ /** Wraps an open ws socket (dialled here, or accepted by a relay server with binaryType "arraybuffer") as a wire-mesh Connection: one canonical-CBOR frame per binary message. */
45
+ export function wrapWsSocket(socket: WsSocket): Connection {
46
+ const pending: Frame[] = [];
47
+ const waiters: {
48
+ resolve: (result: IteratorResult<Frame>) => void;
49
+ reject: (error: unknown) => void;
50
+ }[] = [];
51
+ let ended = false;
52
+ let failure: Error | null = null;
53
+ // Set synchronously the instant close() is called, well before the socket's own readyState transitions are visible to a concurrent caller. Unlike `ended` (only true once the "close" event actually arrives, which a real close handshake delays), this flag exists purely so a send() invoked after close() has started is rejected with a clean, expected error immediately, rather than reaching socket.send() and hitting ws's own "WebSocket is not open: readyState 2 (CLOSING)" (agent-comms#304).
54
+ let closing = false;
55
+ // Every send() call still awaiting its own socket-level callback, so close() can let them settle before tearing the socket down instead of racing a send that was already under way when close() was called.
56
+ const pendingSends = new Set<Promise<void>>();
57
+
58
+ function endAll(): void {
59
+ ended = true;
60
+ for (const waiter of waiters.splice(0)) {
61
+ waiter.resolve({ value: undefined, done: true });
62
+ }
63
+ }
64
+
65
+ function failAll(error: Error): void {
66
+ failure = error;
67
+ ended = true;
68
+ for (const waiter of waiters.splice(0)) {
69
+ waiter.reject(error);
70
+ }
71
+ }
72
+
73
+ socket.on("message", (data) => {
74
+ if (!(data instanceof ArrayBuffer)) {
75
+ failAll(new Error("expected a binary WebSocket message"));
76
+ socket.close(CLOSE_PROTOCOL_ERROR, "protocol error");
77
+ return;
78
+ }
79
+ let frame: Frame;
80
+ try {
81
+ const decoded: unknown = decode(new Uint8Array(data), cdeDecodeOptions);
82
+ const parsed = frameSchema.safeParse(decoded);
83
+ if (!parsed.success) {
84
+ // A decodable-but-unrecognised frame is dropped, keeping the connection -- version negotiation exists to tolerate it.
85
+ return;
86
+ }
87
+ frame = parsed.data;
88
+ } catch (error) {
89
+ failAll(
90
+ error instanceof Error
91
+ ? error
92
+ : new Error(`frame body failed to decode: ${String(error)}`),
93
+ );
94
+ socket.close(CLOSE_PROTOCOL_ERROR, "protocol error");
95
+ return;
96
+ }
97
+ const waiter = waiters.shift();
98
+ if (waiter) {
99
+ waiter.resolve({ value: frame, done: false });
100
+ } else {
101
+ pending.push(frame);
102
+ }
103
+ });
104
+ socket.on("close", endAll);
105
+ socket.on("error", () => {
106
+ // The receive stream's failure signal; the raw error is not otherwise actionable here.
107
+ });
108
+
109
+ const receiveStream: AsyncIterable<Frame> = {
110
+ [Symbol.asyncIterator]() {
111
+ return {
112
+ async next(): Promise<IteratorResult<Frame>> {
113
+ const next = pending.shift();
114
+ if (next !== undefined) {
115
+ return { value: next, done: false };
116
+ }
117
+ if (failure !== null) {
118
+ throw failure;
119
+ }
120
+ if (ended) {
121
+ return { value: undefined, done: true };
122
+ }
123
+ return new Promise<IteratorResult<Frame>>((resolve, reject) => {
124
+ waiters.push({ resolve, reject });
125
+ });
126
+ },
127
+ };
128
+ },
129
+ };
130
+
131
+ return {
132
+ async send(frame: Frame): Promise<void> {
133
+ if (ended || closing) {
134
+ throw new Error("connection is closed");
135
+ }
136
+ const sent = new Promise<void>((resolve, reject) => {
137
+ socket.send(
138
+ new Uint8Array(encode(frame, cdeEncodeOptions)),
139
+ (error) => {
140
+ if (error) {
141
+ reject(error);
142
+ } else {
143
+ resolve();
144
+ }
145
+ },
146
+ );
147
+ });
148
+ // Tracked from the same synchronous tick socket.send() was actually called in, so a close() that starts a moment later always sees this send if it was genuinely already under way.
149
+ const tracked = sent.finally(() => {
150
+ pendingSends.delete(tracked);
151
+ });
152
+ pendingSends.add(tracked);
153
+ return tracked;
154
+ },
155
+ receive: () => receiveStream,
156
+ close: async () => {
157
+ // Reject anything sent from here on before it ever reaches the socket (the send() guard above), and let every send already in flight resolve or reject on its own terms first. Closing the socket out from under an in-flight send is what turns its own ordinary completion into a spurious "WebSocket is not open: readyState 2 (CLOSING)" (agent-comms#304).
158
+ closing = true;
159
+ await Promise.allSettled([...pendingSends]);
160
+ if (ended) return;
161
+ await new Promise<void>((resolve) => {
162
+ socket.once("close", () => {
163
+ resolve();
164
+ });
165
+ socket.close(CLOSE_NORMAL);
166
+ });
167
+ },
168
+ };
169
+ }
170
+
171
+ /** Dials a remote for connectToRemote: a ws:// or wss:// URL through connectWsUrl, anything else as host:port over the given transport. Split out of wire-mesh-transport.ts to keep it under the repo's max-lines cap. */
172
+ export async function dialRemote(
173
+ wireTransport: Readonly<Transport>,
174
+ host: string,
175
+ port: number,
176
+ ): Promise<Connection> {
177
+ // A ws:// or wss:// URL in the host position dials a WebSocket-served
178
+ // hub (e.g. the mesh.exadev.io cloudflare-hub) instead of raw TLS --
179
+ // the port is meaningless in URL form, so callers pass 0. Any other URL
180
+ // scheme is refused here rather than surfacing as an opaque DNS error
181
+ // from the TLS dial treating the whole URL as a hostname.
182
+ const isWsUrl = /^wss?:\/\//.test(host);
183
+ if (!isWsUrl && host.includes("://")) {
184
+ throw new Error(
185
+ `expected a hostname or a ws:// / wss:// URL, got "${host}"`,
186
+ );
187
+ }
188
+ return isWsUrl
189
+ ? connectWsUrl(host)
190
+ : wireTransport.connect(`${host}:${String(port)}`);
191
+ }