wire-mesh-core 0.0.0 → 1.0.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.
Files changed (80) hide show
  1. package/README.md +32 -0
  2. package/dist/adapters/frame-codec.cjs +37 -0
  3. package/dist/adapters/frame-codec.d.cts +12 -0
  4. package/dist/adapters/frame-codec.d.mts +12 -0
  5. package/dist/adapters/frame-codec.mjs +33 -0
  6. package/dist/adapters/memory-storage.cjs +14 -0
  7. package/dist/adapters/memory-storage.d.cts +5 -0
  8. package/dist/adapters/memory-storage.d.mts +5 -0
  9. package/dist/adapters/memory-storage.mjs +13 -0
  10. package/dist/adapters/node-identity.cjs +55 -0
  11. package/dist/adapters/node-identity.d.cts +11 -0
  12. package/dist/adapters/node-identity.d.mts +11 -0
  13. package/dist/adapters/node-identity.mjs +52 -0
  14. package/dist/adapters/system-clock.cjs +8 -0
  15. package/dist/adapters/system-clock.d.cts +5 -0
  16. package/dist/adapters/system-clock.d.mts +5 -0
  17. package/dist/adapters/system-clock.mjs +7 -0
  18. package/dist/adapters/tcp-transport.cjs +154 -0
  19. package/dist/adapters/tcp-transport.d.cts +5 -0
  20. package/dist/adapters/tcp-transport.d.mts +5 -0
  21. package/dist/adapters/tcp-transport.mjs +153 -0
  22. package/dist/adapters/tls-transport.cjs +178 -0
  23. package/dist/adapters/tls-transport.d.cts +9 -0
  24. package/dist/adapters/tls-transport.d.mts +9 -0
  25. package/dist/adapters/tls-transport.mjs +177 -0
  26. package/dist/clock-DiSx-WKM.d.cts +7 -0
  27. package/dist/clock-DiSx-WKM.d.mts +7 -0
  28. package/dist/domain/device-id.cjs +27 -0
  29. package/dist/domain/device-id.d.cts +9 -0
  30. package/dist/domain/device-id.d.mts +9 -0
  31. package/dist/domain/device-id.mjs +24 -0
  32. package/dist/domain/handshake.cjs +23 -0
  33. package/dist/domain/handshake.d.cts +16 -0
  34. package/dist/domain/handshake.d.mts +16 -0
  35. package/dist/domain/handshake.mjs +21 -0
  36. package/dist/domain/mesh-session.cjs +439 -0
  37. package/dist/domain/mesh-session.d.cts +101 -0
  38. package/dist/domain/mesh-session.d.mts +101 -0
  39. package/dist/domain/mesh-session.mjs +436 -0
  40. package/dist/domain/relay-hub.cjs +99 -0
  41. package/dist/domain/relay-hub.d.cts +10 -0
  42. package/dist/domain/relay-hub.d.mts +10 -0
  43. package/dist/domain/relay-hub.mjs +98 -0
  44. package/dist/domain/revocation-view.cjs +22 -0
  45. package/dist/domain/revocation-view.d.cts +12 -0
  46. package/dist/domain/revocation-view.d.mts +12 -0
  47. package/dist/domain/revocation-view.mjs +21 -0
  48. package/dist/domain/tokens.cjs +300 -0
  49. package/dist/domain/tokens.d.cts +86 -0
  50. package/dist/domain/tokens.d.mts +86 -0
  51. package/dist/domain/tokens.mjs +296 -0
  52. package/dist/generated/protocol.cjs +466 -0
  53. package/dist/generated/protocol.d.cts +2 -0
  54. package/dist/generated/protocol.d.mts +2 -0
  55. package/dist/generated/protocol.mjs +382 -0
  56. package/dist/generated/runtime.cjs +8 -0
  57. package/dist/generated/runtime.d.cts +2 -0
  58. package/dist/generated/runtime.d.mts +2 -0
  59. package/dist/generated/runtime.mjs +2 -0
  60. package/dist/identity-BRLEUfVY.d.cts +17 -0
  61. package/dist/identity-qNkmGytv.d.mts +17 -0
  62. package/dist/ports/clock.cjs +0 -0
  63. package/dist/ports/clock.d.cts +2 -0
  64. package/dist/ports/clock.d.mts +2 -0
  65. package/dist/ports/clock.mjs +1 -0
  66. package/dist/ports/identity.cjs +0 -0
  67. package/dist/ports/identity.d.cts +2 -0
  68. package/dist/ports/identity.d.mts +2 -0
  69. package/dist/ports/identity.mjs +1 -0
  70. package/dist/ports/storage.cjs +0 -0
  71. package/dist/ports/storage.d.cts +9 -0
  72. package/dist/ports/storage.d.mts +9 -0
  73. package/dist/ports/storage.mjs +1 -0
  74. package/dist/ports/transport.cjs +0 -0
  75. package/dist/ports/transport.d.cts +29 -0
  76. package/dist/ports/transport.d.mts +29 -0
  77. package/dist/ports/transport.mjs +1 -0
  78. package/dist/protocol-B26-5VX7.d.cts +1112 -0
  79. package/dist/protocol-B26-5VX7.d.mts +1112 -0
  80. package/package.json +130 -2
@@ -0,0 +1,101 @@
1
+ import { F as ManageOk, M as ManageCommand, P as ManageError, U as PeerAdvert, Z as ProtocolVersion, ct as RevocationEntry, i as CapabilityToken, r as CapabilityScope, v as DeviceId, w as Frame } from "../protocol-B26-5VX7.mjs";
2
+ import { t as IdentityPort } from "../identity-qNkmGytv.mjs";
3
+ import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
+ import { Connection, Transport } from "../ports/transport.mjs";
5
+ //#region src/domain/mesh-session.d.ts
6
+ /** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
7
+ export declare const HANDSHAKE_TIMEOUT_MS = 3000;
8
+ export type ConnectionState = {
9
+ status: "idle";
10
+ } | {
11
+ status: "connecting";
12
+ address: string;
13
+ } | {
14
+ status: "connected";
15
+ address: string;
16
+ handshake: HandshakeStatus;
17
+ } | {
18
+ status: "reconnecting";
19
+ address: string;
20
+ attempt: number;
21
+ reason: string;
22
+ } | {
23
+ status: "closed";
24
+ address: string;
25
+ reason: string;
26
+ };
27
+ /** Opt-in retry behaviour for a session's own reconnect attempts. Left at its default `null` in createMeshSession, a disconnect always falls straight to `"closed"` -- today's exact behaviour, unchanged unless a caller opts in. */
28
+ export interface ReconnectPolicy {
29
+ maxAttempts: number;
30
+ /** Backoff before attempt N (1-indexed), in milliseconds. */
31
+ delayMs: (attempt: number) => number;
32
+ }
33
+ /** The union `manage-response-frame.outcome` actually carries -- no dedicated generated type name exists for it. */
34
+ export type ManageOutcome = ManageOk | ManageError;
35
+ /** One `manage-request` this session received from its peer, surfaced for the caller to act on and answer. */
36
+ export interface IncomingManageRequest {
37
+ requestId: number;
38
+ command: ManageCommand;
39
+ scope: CapabilityScope;
40
+ token?: CapabilityToken;
41
+ /** The device-id of the peer this request was relayed on behalf of, present only when the request arrived wrapped in a relay-data frame rather than directly over this session's own connection. A caller that needs to address a further request back to the same peer (one not sent via respond(), which already routes back correctly on its own) passes this as sendManageRequest's targetDevice. */
42
+ fromDevice?: DeviceId;
43
+ respond: (outcome: ManageOutcome) => Promise<void>;
44
+ }
45
+ export type HandshakeStatus = {
46
+ status: "pending";
47
+ } | {
48
+ status: "negotiated";
49
+ version: ProtocolVersion;
50
+ sharedDomains: string[];
51
+ } | {
52
+ status: "unanswered";
53
+ } | {
54
+ status: "rejected";
55
+ reason: string;
56
+ };
57
+ export interface SessionEvent {
58
+ state: ConnectionState;
59
+ /** The peer directory as of this event: latest peer-advert per device-id, in first-heard order. */
60
+ directory: readonly DirectoryEntry[];
61
+ /** Every frame that crossed the connection, sent or received, in order. */
62
+ frameLog: readonly FrameLogEntry[];
63
+ }
64
+ export interface DirectoryEntry {
65
+ device: DeviceId;
66
+ advert: PeerAdvert;
67
+ }
68
+ export interface FrameLogEntry {
69
+ direction: "sent" | "received";
70
+ frame: Frame;
71
+ }
72
+ export interface MeshSession {
73
+ readonly events: AsyncIterable<SessionEvent>;
74
+ /** Every `manage-request` received from the peer, in arrival order. */
75
+ readonly incomingManageRequests: AsyncIterable<IncomingManageRequest>;
76
+ /** Every `revocation-entry` received from the peer, in arrival order -- a `revocation-announce` frame's own `entries` array is flattened to one item per entry, since each entry is independently verifiable and independently meaningful regardless of which frame carried it. A consumer typically feeds each one into a RevocationView's own `record`. */
77
+ readonly revocationAnnouncements: AsyncIterable<RevocationEntry>;
78
+ connect: (address: string, localDomains: readonly string[]) => Promise<void>;
79
+ sendPing: () => Promise<void>;
80
+ /** Attaches this token to every `manage-request` sent from now on. */
81
+ setToken: (token: CapabilityToken) => void;
82
+ /** Announces one or more already-minted revocation-entries to the peer. Sent directly over the connection, never relay-wrapped -- revocation-announce is a gossiped broadcast, not a request addressed to a specific peer, so it has no targetDevice/token parameters the way sendManageRequest does. */
83
+ sendRevocationAnnounce: (entries: readonly RevocationEntry[]) => Promise<void>;
84
+ /** Sends a manage-request and resolves with the matching manage-response's outcome, correlated by request-id. When targetDevice is given, the request is routed to that specific peer via an established relay-connect pairing (wrapped as relay-data) rather than sent directly over this session's own Connection -- relay-hub deliberately drops manage-request/manage-response frames sent to it directly, since routing between two connected peers is not the relay role's business, so a specific peer reachable only through a relay hub can only be addressed this way. Absent, this sends directly over the Connection exactly as before. When token is given, it is attached to this one request instead of whatever setToken last set -- a single session routinely needs a different token per request when its peer shares more than one scope with this side (e.g. several core/room memberships over one connection), and a session-global token can only ever be correct for one of them. Absent, this request carries setToken's own session-global token exactly as before. */
85
+ sendManageRequest: (command: ManageCommand, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, token?: CapabilityToken) => Promise<ManageOutcome>;
86
+ close: () => Promise<void>;
87
+ }
88
+ export declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null): MeshSession;
89
+ /** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
90
+ export interface AcceptedMeshSession extends MeshSession {
91
+ /** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
92
+ peerDeviceId: Promise<DeviceId>;
93
+ }
94
+ export interface AcceptedMeshSessionOptions {
95
+ /** A caller-chosen label for this connection, used only for ConnectionState's own address field -- the Connection/Transport ports expose no remote-address concept an accepted connection could report on its own (see wire-mesh#45). Defaults to a fixed placeholder since most callers have nothing more specific to offer; a transport adapter that does know the remote's address should pass it here. */
96
+ label?: string;
97
+ clock?: Readonly<Clock>;
98
+ }
99
+ /** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
100
+ export declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
101
+ //#endregion
@@ -0,0 +1,436 @@
1
+ import { deviceIdToHex } from "./device-id.mjs";
2
+ import { negotiate } from "./handshake.mjs";
3
+ import { messageFromFrame, tryDecodeFrame } from "../adapters/frame-codec.mjs";
4
+ //#region src/domain/mesh-session.ts
5
+ const MS_PER_SECOND = 1e3;
6
+ /** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
7
+ const HANDSHAKE_TIMEOUT_MS = 3e3;
8
+ function localHandshake(domains) {
9
+ return {
10
+ type: "handshake",
11
+ version: 1,
12
+ domains: [...domains]
13
+ };
14
+ }
15
+ /**
16
+ * Builds the connection-agnostic session state machine and its public MeshSession surface. dial is null for a session that can never (re)connect on its own -- acceptMeshSession's case, where the one connection it will ever have already exists by construction and reconnect therefore cannot apply (only the remote redialing, and being accepted again, produces a fresh connection). createMeshSession supplies dial as transport.connect so its own connect()/reconnect behaviour is unchanged from before this was factored out.
17
+ */
18
+ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
19
+ let connection = null;
20
+ let state = { status: "idle" };
21
+ let handshake = { status: "pending" };
22
+ const directory = /* @__PURE__ */ new Map();
23
+ const frameLog = [];
24
+ let feedCancelled = false;
25
+ const eventWaiters = [];
26
+ const eventBacklog = [];
27
+ let handshakeTimer = null;
28
+ let reconnectTimer = null;
29
+ let attempt = 0;
30
+ let currentToken = null;
31
+ let nextRequestId = 0;
32
+ let relayPeerDevice = null;
33
+ const pendingManageRequests = /* @__PURE__ */ new Map();
34
+ const incomingWaiters = [];
35
+ const incomingBacklog = [];
36
+ const revocationWaiters = [];
37
+ const revocationBacklog = [];
38
+ function snapshot() {
39
+ return {
40
+ state,
41
+ directory: [...directory.values()],
42
+ frameLog: [...frameLog]
43
+ };
44
+ }
45
+ function emit() {
46
+ const event = snapshot();
47
+ const waiter = eventWaiters.shift();
48
+ if (waiter) waiter(event);
49
+ else eventBacklog.push(event);
50
+ }
51
+ function emitIncomingManageRequest(request) {
52
+ const waiter = incomingWaiters.shift();
53
+ if (waiter) waiter(request);
54
+ else incomingBacklog.push(request);
55
+ }
56
+ function emitRevocationEntry(entry) {
57
+ const waiter = revocationWaiters.shift();
58
+ if (waiter) waiter(entry);
59
+ else revocationBacklog.push(entry);
60
+ }
61
+ function rejectPendingManageRequests(reason) {
62
+ for (const pending of pendingManageRequests.values()) pending.reject(new Error(reason));
63
+ pendingManageRequests.clear();
64
+ }
65
+ function buildManageRequest(command, scope, tokenOverride) {
66
+ const requestId = nextRequestId;
67
+ nextRequestId += 1;
68
+ const token = tokenOverride ?? currentToken;
69
+ return {
70
+ type: "manage-request",
71
+ "request-id": requestId,
72
+ command,
73
+ scope,
74
+ ...token !== null ? { token } : {}
75
+ };
76
+ }
77
+ /** Sends a frame, wrapping it as relay-data first when viaRelay is set -- the single choke point every outbound manage-request/manage-response passes through, so a consumer of sendManageRequest/respond never needs its own relay-wrapping logic. */
78
+ async function transmit(frame, viaRelay) {
79
+ if (connection === null) throw new Error("not connected");
80
+ if (viaRelay) {
81
+ const relayFrame = {
82
+ type: "relay-data",
83
+ payload: messageFromFrame(frame)
84
+ };
85
+ await connection.send(relayFrame);
86
+ return;
87
+ }
88
+ await connection.send(frame);
89
+ }
90
+ function applyManageResponse(frame) {
91
+ const requestId = frame["request-id"];
92
+ const pending = pendingManageRequests.get(requestId);
93
+ if (pending !== void 0) {
94
+ pendingManageRequests.delete(requestId);
95
+ pending.resolve(frame.outcome);
96
+ }
97
+ }
98
+ function applyManageRequest(frame, viaRelay) {
99
+ const requestId = frame["request-id"];
100
+ const fromDevice = viaRelay && relayPeerDevice !== null ? relayPeerDevice : void 0;
101
+ emitIncomingManageRequest({
102
+ requestId,
103
+ command: frame.command,
104
+ scope: frame.scope,
105
+ ...frame.token !== void 0 ? { token: frame.token } : {},
106
+ ...fromDevice !== void 0 ? { fromDevice } : {},
107
+ respond: async (outcome) => {
108
+ const response = {
109
+ type: "manage-response",
110
+ "request-id": requestId,
111
+ outcome
112
+ };
113
+ frameLog.push({
114
+ direction: "sent",
115
+ frame: response
116
+ });
117
+ await transmit(response, viaRelay);
118
+ emit();
119
+ }
120
+ });
121
+ }
122
+ function applyFrame(frame) {
123
+ if (frame.type === "relay-data") {
124
+ const inner = tryDecodeFrame(frame.payload);
125
+ if (inner !== null && (inner.type === "manage-request" || inner.type === "manage-response")) {
126
+ frameLog.push({
127
+ direction: "received",
128
+ frame: inner
129
+ });
130
+ if (inner.type === "manage-response") applyManageResponse(inner);
131
+ else applyManageRequest(inner, true);
132
+ return;
133
+ }
134
+ frameLog.push({
135
+ direction: "received",
136
+ frame
137
+ });
138
+ return;
139
+ }
140
+ frameLog.push({
141
+ direction: "received",
142
+ frame
143
+ });
144
+ if (frame.type === "handshake") applyRemoteHandshake(frame);
145
+ else if (frame.type === "gossip") for (const advert of frame.peers) {
146
+ directory.set(deviceIdToHex(advert.device), {
147
+ device: advert.device,
148
+ advert
149
+ });
150
+ onPeerAdvert?.(advert);
151
+ }
152
+ else if (frame.type === "relay-inbound") relayPeerDevice = frame["source-device"];
153
+ else if (frame.type === "manage-response") applyManageResponse(frame);
154
+ else if (frame.type === "manage-request") applyManageRequest(frame, false);
155
+ else if (frame.type === "revocation-announce") for (const entry of frame.entries) emitRevocationEntry(entry);
156
+ }
157
+ /** Establishes a relay-connect pairing to targetDevice if this session isn't already paired with it -- a no-op when it already is, whether that pairing was established by this session's own prior relay-connect (initiator role) or learned from an incoming relay-inbound (target role, replying back to whoever dialed it). relay-connect has no ack frame: the initiator proceeds to relay-data right after sending it. */
158
+ async function ensureRelayPairing(targetDevice) {
159
+ if (connection === null) throw new Error("not connected");
160
+ if (relayPeerDevice !== null && deviceIdToHex(relayPeerDevice) === deviceIdToHex(targetDevice)) return;
161
+ const relayConnect = {
162
+ type: "relay-connect",
163
+ "target-device": targetDevice
164
+ };
165
+ frameLog.push({
166
+ direction: "sent",
167
+ frame: relayConnect
168
+ });
169
+ await connection.send(relayConnect);
170
+ relayPeerDevice = targetDevice;
171
+ emit();
172
+ }
173
+ let localHandshakeSent = localHandshake([]);
174
+ function applyRemoteHandshake(remote) {
175
+ if (handshake.status !== "pending") return;
176
+ if (handshakeTimer !== null) {
177
+ clearTimeout(handshakeTimer);
178
+ handshakeTimer = null;
179
+ }
180
+ const result = negotiate(localHandshakeSent, remote);
181
+ handshake = result.ok ? {
182
+ status: "negotiated",
183
+ version: result.version,
184
+ sharedDomains: result.sharedDomains
185
+ } : {
186
+ status: "rejected",
187
+ reason: "no shared domains or version"
188
+ };
189
+ if (state.status === "connected") state = {
190
+ ...state,
191
+ handshake
192
+ };
193
+ }
194
+ function handleDisconnect(reason, address, localDomains) {
195
+ if (feedCancelled) return;
196
+ rejectPendingManageRequests("disconnected before a response arrived");
197
+ if (reconnect !== null && attempt < reconnect.maxAttempts) {
198
+ attempt += 1;
199
+ const currentAttempt = attempt;
200
+ state = {
201
+ status: "reconnecting",
202
+ address,
203
+ attempt: currentAttempt,
204
+ reason
205
+ };
206
+ emit();
207
+ reconnectTimer = setTimeout(() => {
208
+ reconnectTimer = null;
209
+ doConnect(address, localDomains).catch((error) => {
210
+ handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
211
+ });
212
+ }, reconnect.delayMs(currentAttempt));
213
+ return;
214
+ }
215
+ state = {
216
+ status: "closed",
217
+ address,
218
+ reason
219
+ };
220
+ emit();
221
+ }
222
+ async function consume(link, address, localDomains) {
223
+ for await (const frame of link.receive()) {
224
+ if (feedCancelled) return;
225
+ applyFrame(frame);
226
+ emit();
227
+ }
228
+ if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
229
+ }
230
+ /** Everything a connection needs once it exists, regardless of whether it was dialled (createMeshSession's own doConnect, below) or handed over already established (acceptMeshSession): send this side's handshake and self-advert, arm the handshake timeout, and start consuming frames. The two entry points differ only in how link itself came to exist and what address means for it -- a real dial target for one, a caller-chosen label for the other, since the Connection/Transport ports expose no remote-address concept of their own for an accepted connection. */
231
+ async function wireUpConnection(link, address, localDomains) {
232
+ connection = link;
233
+ localHandshakeSent = localHandshake(localDomains);
234
+ handshake = { status: "pending" };
235
+ state = {
236
+ status: "connected",
237
+ address,
238
+ handshake
239
+ };
240
+ frameLog.push({
241
+ direction: "sent",
242
+ frame: localHandshakeSent
243
+ });
244
+ await connection.send(localHandshakeSent);
245
+ emit();
246
+ const selfAdvert = {
247
+ type: "gossip",
248
+ peers: [{
249
+ device: identity.deviceId,
250
+ addresses: [],
251
+ "snapshot-seconds": Math.floor(clock.now() / MS_PER_SECOND)
252
+ }]
253
+ };
254
+ frameLog.push({
255
+ direction: "sent",
256
+ frame: selfAdvert
257
+ });
258
+ await connection.send(selfAdvert);
259
+ emit();
260
+ handshakeTimer = setTimeout(() => {
261
+ handshakeTimer = null;
262
+ if (handshake.status === "pending") {
263
+ handshake = { status: "unanswered" };
264
+ if (state.status === "connected") state = {
265
+ ...state,
266
+ handshake
267
+ };
268
+ emit();
269
+ }
270
+ }, HANDSHAKE_TIMEOUT_MS);
271
+ consume(connection, address, localDomains).catch((error) => {
272
+ if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
273
+ });
274
+ }
275
+ async function doConnect(address, localDomains) {
276
+ if (dial === null) throw new Error("this session is already connected; there is nothing to dial");
277
+ if (handshakeTimer !== null) {
278
+ clearTimeout(handshakeTimer);
279
+ handshakeTimer = null;
280
+ }
281
+ state = {
282
+ status: "connecting",
283
+ address
284
+ };
285
+ emit();
286
+ const link = await dial(address);
287
+ if (feedCancelled) {
288
+ await link.close();
289
+ return;
290
+ }
291
+ await wireUpConnection(link, address, localDomains);
292
+ }
293
+ return {
294
+ wireUpConnection,
295
+ session: {
296
+ events: { [Symbol.asyncIterator]() {
297
+ return { next: async () => new Promise((resolve) => {
298
+ const backlogEvent = eventBacklog.shift();
299
+ if (backlogEvent) resolve({
300
+ value: backlogEvent,
301
+ done: false
302
+ });
303
+ else eventWaiters.push((event) => {
304
+ resolve({
305
+ value: event,
306
+ done: false
307
+ });
308
+ });
309
+ }) };
310
+ } },
311
+ incomingManageRequests: { [Symbol.asyncIterator]() {
312
+ return { next: async () => new Promise((resolve) => {
313
+ const backlogRequest = incomingBacklog.shift();
314
+ if (backlogRequest) resolve({
315
+ value: backlogRequest,
316
+ done: false
317
+ });
318
+ else incomingWaiters.push((request) => {
319
+ resolve({
320
+ value: request,
321
+ done: false
322
+ });
323
+ });
324
+ }) };
325
+ } },
326
+ revocationAnnouncements: { [Symbol.asyncIterator]() {
327
+ return { next: async () => new Promise((resolve) => {
328
+ const backlogEntry = revocationBacklog.shift();
329
+ if (backlogEntry) resolve({
330
+ value: backlogEntry,
331
+ done: false
332
+ });
333
+ else revocationWaiters.push((entry) => {
334
+ resolve({
335
+ value: entry,
336
+ done: false
337
+ });
338
+ });
339
+ }) };
340
+ } },
341
+ async connect(address, localDomains) {
342
+ if (connection !== null) throw new Error("a session connects once; create a new one to reconnect");
343
+ attempt = 0;
344
+ await doConnect(address, localDomains);
345
+ },
346
+ async sendPing() {
347
+ if (connection === null || state.status !== "connected") throw new Error("not connected");
348
+ const ping = { type: "ping" };
349
+ frameLog.push({
350
+ direction: "sent",
351
+ frame: ping
352
+ });
353
+ await connection.send(ping);
354
+ emit();
355
+ },
356
+ setToken(token) {
357
+ currentToken = token;
358
+ },
359
+ async sendManageRequest(command, scope, targetDevice, token) {
360
+ if (connection === null || state.status !== "connected") throw new Error("not connected");
361
+ if (targetDevice !== void 0) await ensureRelayPairing(targetDevice);
362
+ const frame = buildManageRequest(command, scope, token);
363
+ const outcome = new Promise((resolve, reject) => {
364
+ pendingManageRequests.set(frame["request-id"], {
365
+ resolve,
366
+ reject
367
+ });
368
+ });
369
+ frameLog.push({
370
+ direction: "sent",
371
+ frame
372
+ });
373
+ await transmit(frame, targetDevice !== void 0);
374
+ emit();
375
+ return outcome;
376
+ },
377
+ async sendRevocationAnnounce(entries) {
378
+ if (connection === null || state.status !== "connected") throw new Error("not connected");
379
+ const frame = {
380
+ type: "revocation-announce",
381
+ entries: [...entries]
382
+ };
383
+ frameLog.push({
384
+ direction: "sent",
385
+ frame
386
+ });
387
+ await transmit(frame, false);
388
+ emit();
389
+ },
390
+ async close() {
391
+ feedCancelled = true;
392
+ if (handshakeTimer !== null) {
393
+ clearTimeout(handshakeTimer);
394
+ handshakeTimer = null;
395
+ }
396
+ if (reconnectTimer !== null) {
397
+ clearTimeout(reconnectTimer);
398
+ reconnectTimer = null;
399
+ }
400
+ rejectPendingManageRequests("connection closed before a response arrived");
401
+ if (connection !== null) await connection.close();
402
+ if (state.status === "connected" || state.status === "connecting" || state.status === "reconnecting") state = {
403
+ status: "closed",
404
+ address: state.address,
405
+ reason: "closed by you"
406
+ };
407
+ emit();
408
+ }
409
+ }
410
+ };
411
+ }
412
+ function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null) {
413
+ const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address));
414
+ return session;
415
+ }
416
+ /** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
417
+ async function acceptMeshSession(connection, identity, localDomains, options = {}) {
418
+ const clock = options.clock ?? { now: () => Date.now() };
419
+ let resolvePeerDeviceId = null;
420
+ const peerDeviceId = new Promise((resolve) => {
421
+ resolvePeerDeviceId = resolve;
422
+ });
423
+ let peerDeviceIdResolved = false;
424
+ const { session, wireUpConnection } = createSessionCore(identity, clock, null, null, (advert) => {
425
+ if (peerDeviceIdResolved) return;
426
+ peerDeviceIdResolved = true;
427
+ resolvePeerDeviceId?.(advert.device);
428
+ });
429
+ await wireUpConnection(connection, options.label ?? "accepted", localDomains);
430
+ return {
431
+ ...session,
432
+ peerDeviceId
433
+ };
434
+ }
435
+ //#endregion
436
+ export { HANDSHAKE_TIMEOUT_MS, acceptMeshSession, createMeshSession };
@@ -0,0 +1,99 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/domain/relay-hub.ts
3
+ const HEX_RADIX = 16;
4
+ const HEX_DIGITS_PER_BYTE = 2;
5
+ function deviceKey(device) {
6
+ let key = "";
7
+ for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
8
+ return key;
9
+ }
10
+ function createRelayHub() {
11
+ const devices = /* @__PURE__ */ new Map();
12
+ const connectionDevice = /* @__PURE__ */ new Map();
13
+ const pairings = /* @__PURE__ */ new Map();
14
+ const mostRecentPairing = /* @__PURE__ */ new Map();
15
+ function pairingsOf(connection) {
16
+ const existing = pairings.get(connection);
17
+ if (existing) return existing;
18
+ const created = /* @__PURE__ */ new Map();
19
+ pairings.set(connection, created);
20
+ return created;
21
+ }
22
+ function addPairing(a, aDevice, b, bDevice) {
23
+ pairingsOf(a).set(deviceKey(bDevice), b);
24
+ pairingsOf(b).set(deviceKey(aDevice), a);
25
+ mostRecentPairing.set(a, b);
26
+ mostRecentPairing.set(b, a);
27
+ }
28
+ function forgetConnection(connection) {
29
+ for (const [key, registration] of devices) if (registration.connection === connection) devices.delete(key);
30
+ const ownDevice = connectionDevice.get(connection);
31
+ connectionDevice.delete(connection);
32
+ mostRecentPairing.delete(connection);
33
+ const own = pairings.get(connection);
34
+ if (own) {
35
+ for (const peer of own.values()) {
36
+ const peerOwn = pairings.get(peer);
37
+ if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
38
+ if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
39
+ }
40
+ pairings.delete(connection);
41
+ }
42
+ }
43
+ async function handleFrame(connection, frame) {
44
+ if (frame.type === "gossip") {
45
+ for (const advert of frame.peers) devices.set(deviceKey(advert.device), {
46
+ connection,
47
+ device: advert.device
48
+ });
49
+ for (const registration of devices.values()) if (registration.connection === connection) {
50
+ connectionDevice.set(connection, registration.device);
51
+ break;
52
+ }
53
+ return;
54
+ }
55
+ if (frame.type === "relay-connect") {
56
+ const registration = devices.get(deviceKey(frame["target-device"]));
57
+ if (!registration || registration.connection === connection) return;
58
+ const initiatorDevice = connectionDevice.get(connection);
59
+ if (initiatorDevice === void 0) return;
60
+ addPairing(connection, initiatorDevice, registration.connection, registration.device);
61
+ await registration.connection.send({
62
+ type: "relay-inbound",
63
+ "source-device": initiatorDevice
64
+ });
65
+ return;
66
+ }
67
+ if (frame.type === "relay-data") {
68
+ const own = pairings.get(connection);
69
+ const toDevice = frame["to-device"];
70
+ const peer = toDevice !== void 0 ? own?.get(deviceKey(toDevice)) : mostRecentPairing.get(connection);
71
+ if (!peer) return;
72
+ const senderDevice = connectionDevice.get(connection);
73
+ if (senderDevice === void 0) return;
74
+ await peer.send({
75
+ type: "relay-data",
76
+ payload: frame.payload,
77
+ "from-device": senderDevice
78
+ });
79
+ return;
80
+ }
81
+ }
82
+ return {
83
+ async handleConnection(connection) {
84
+ try {
85
+ for await (const frame of connection.receive()) await handleFrame(connection, frame);
86
+ } catch {} finally {
87
+ forgetConnection(connection);
88
+ }
89
+ },
90
+ stop() {
91
+ devices.clear();
92
+ connectionDevice.clear();
93
+ pairings.clear();
94
+ mostRecentPairing.clear();
95
+ }
96
+ };
97
+ }
98
+ //#endregion
99
+ exports.createRelayHub = createRelayHub;
@@ -0,0 +1,10 @@
1
+ import { Connection } from "../ports/transport.cjs";
2
+ //#region src/domain/relay-hub.d.ts
3
+ export interface RelayHub {
4
+ /** Drives one accepted connection until it closes: registers gossip-advertised devices, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. */
5
+ handleConnection: (connection: Readonly<Connection>) => Promise<void>;
6
+ /** Drops all registry and pairing state -- used by tests and by transport teardown. */
7
+ stop: () => void;
8
+ }
9
+ export declare function createRelayHub(): RelayHub;
10
+ //#endregion
@@ -0,0 +1,10 @@
1
+ import { Connection } from "../ports/transport.mjs";
2
+ //#region src/domain/relay-hub.d.ts
3
+ export interface RelayHub {
4
+ /** Drives one accepted connection until it closes: registers gossip-advertised devices, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. */
5
+ handleConnection: (connection: Readonly<Connection>) => Promise<void>;
6
+ /** Drops all registry and pairing state -- used by tests and by transport teardown. */
7
+ stop: () => void;
8
+ }
9
+ export declare function createRelayHub(): RelayHub;
10
+ //#endregion