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.
- package/README.md +32 -0
- package/dist/adapters/frame-codec.cjs +37 -0
- package/dist/adapters/frame-codec.d.cts +12 -0
- package/dist/adapters/frame-codec.d.mts +12 -0
- package/dist/adapters/frame-codec.mjs +33 -0
- package/dist/adapters/memory-storage.cjs +14 -0
- package/dist/adapters/memory-storage.d.cts +5 -0
- package/dist/adapters/memory-storage.d.mts +5 -0
- package/dist/adapters/memory-storage.mjs +13 -0
- package/dist/adapters/node-identity.cjs +55 -0
- package/dist/adapters/node-identity.d.cts +11 -0
- package/dist/adapters/node-identity.d.mts +11 -0
- package/dist/adapters/node-identity.mjs +52 -0
- package/dist/adapters/system-clock.cjs +8 -0
- package/dist/adapters/system-clock.d.cts +5 -0
- package/dist/adapters/system-clock.d.mts +5 -0
- package/dist/adapters/system-clock.mjs +7 -0
- package/dist/adapters/tcp-transport.cjs +154 -0
- package/dist/adapters/tcp-transport.d.cts +5 -0
- package/dist/adapters/tcp-transport.d.mts +5 -0
- package/dist/adapters/tcp-transport.mjs +153 -0
- package/dist/adapters/tls-transport.cjs +178 -0
- package/dist/adapters/tls-transport.d.cts +9 -0
- package/dist/adapters/tls-transport.d.mts +9 -0
- package/dist/adapters/tls-transport.mjs +177 -0
- package/dist/clock-DiSx-WKM.d.cts +7 -0
- package/dist/clock-DiSx-WKM.d.mts +7 -0
- package/dist/domain/device-id.cjs +27 -0
- package/dist/domain/device-id.d.cts +9 -0
- package/dist/domain/device-id.d.mts +9 -0
- package/dist/domain/device-id.mjs +24 -0
- package/dist/domain/handshake.cjs +23 -0
- package/dist/domain/handshake.d.cts +16 -0
- package/dist/domain/handshake.d.mts +16 -0
- package/dist/domain/handshake.mjs +21 -0
- package/dist/domain/mesh-session.cjs +439 -0
- package/dist/domain/mesh-session.d.cts +101 -0
- package/dist/domain/mesh-session.d.mts +101 -0
- package/dist/domain/mesh-session.mjs +436 -0
- package/dist/domain/relay-hub.cjs +99 -0
- package/dist/domain/relay-hub.d.cts +10 -0
- package/dist/domain/relay-hub.d.mts +10 -0
- package/dist/domain/relay-hub.mjs +98 -0
- package/dist/domain/revocation-view.cjs +22 -0
- package/dist/domain/revocation-view.d.cts +12 -0
- package/dist/domain/revocation-view.d.mts +12 -0
- package/dist/domain/revocation-view.mjs +21 -0
- package/dist/domain/tokens.cjs +300 -0
- package/dist/domain/tokens.d.cts +86 -0
- package/dist/domain/tokens.d.mts +86 -0
- package/dist/domain/tokens.mjs +296 -0
- package/dist/generated/protocol.cjs +466 -0
- package/dist/generated/protocol.d.cts +2 -0
- package/dist/generated/protocol.d.mts +2 -0
- package/dist/generated/protocol.mjs +382 -0
- package/dist/generated/runtime.cjs +8 -0
- package/dist/generated/runtime.d.cts +2 -0
- package/dist/generated/runtime.d.mts +2 -0
- package/dist/generated/runtime.mjs +2 -0
- package/dist/identity-BRLEUfVY.d.cts +17 -0
- package/dist/identity-qNkmGytv.d.mts +17 -0
- package/dist/ports/clock.cjs +0 -0
- package/dist/ports/clock.d.cts +2 -0
- package/dist/ports/clock.d.mts +2 -0
- package/dist/ports/clock.mjs +1 -0
- package/dist/ports/identity.cjs +0 -0
- package/dist/ports/identity.d.cts +2 -0
- package/dist/ports/identity.d.mts +2 -0
- package/dist/ports/identity.mjs +1 -0
- package/dist/ports/storage.cjs +0 -0
- package/dist/ports/storage.d.cts +9 -0
- package/dist/ports/storage.d.mts +9 -0
- package/dist/ports/storage.mjs +1 -0
- package/dist/ports/transport.cjs +0 -0
- package/dist/ports/transport.d.cts +29 -0
- package/dist/ports/transport.d.mts +29 -0
- package/dist/ports/transport.mjs +1 -0
- package/dist/protocol-B26-5VX7.d.cts +1112 -0
- package/dist/protocol-B26-5VX7.d.mts +1112 -0
- package/package.json +130 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Z as ProtocolVersion, k as HandshakeFrame, x as DomainId } from "../protocol-B26-5VX7.mjs";
|
|
2
|
+
//#region src/domain/handshake.d.ts
|
|
3
|
+
/** The highest protocol version this build of core understands. */
|
|
4
|
+
export declare const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion;
|
|
5
|
+
export interface NegotiationResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
/** The version both peers will speak for the rest of the session -- the lower of the two offered versions, so a peer never has to understand a frame shape it didn't advertise. */
|
|
8
|
+
version: ProtocolVersion;
|
|
9
|
+
/** Domains both peers advertised and that are not retired -- the only ones either side may address for the rest of the session. */
|
|
10
|
+
sharedDomains: DomainId[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Negotiates protocol version and capability domains between a local and remote handshake -- the mechanism wire-mesh's handshake exists to provide, and agent-comms issue #31's fix: a mixed fleet of old and new peers negotiates down to what they both actually support, rather than one side silently misinterpreting frames the other can't produce yet.
|
|
14
|
+
*/
|
|
15
|
+
export declare function negotiate(local: HandshakeFrame, remote: HandshakeFrame): NegotiationResult;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/domain/handshake.ts
|
|
2
|
+
/** The highest protocol version this build of core understands. */
|
|
3
|
+
const SUPPORTED_PROTOCOL_VERSION = 1;
|
|
4
|
+
/**
|
|
5
|
+
* Domain names that must never be negotiated, per handshake.cddl: core/federation is retired (its string stays reserved, never reused) and "a peer must never advertise or negotiate it". Excluded here even if both peers advertise it -- two buggy advertisers must not end up speaking a dead domain.
|
|
6
|
+
*/
|
|
7
|
+
const RETIRED_DOMAINS = ["core/federation"];
|
|
8
|
+
/**
|
|
9
|
+
* Negotiates protocol version and capability domains between a local and remote handshake -- the mechanism wire-mesh's handshake exists to provide, and agent-comms issue #31's fix: a mixed fleet of old and new peers negotiates down to what they both actually support, rather than one side silently misinterpreting frames the other can't produce yet.
|
|
10
|
+
*/
|
|
11
|
+
function negotiate(local, remote) {
|
|
12
|
+
const version = Math.min(local.version, remote.version);
|
|
13
|
+
const sharedDomains = local.domains.filter((domain) => !RETIRED_DOMAINS.includes(domain) && remote.domains.includes(domain));
|
|
14
|
+
return {
|
|
15
|
+
ok: version >= 1 && sharedDomains.length > 0,
|
|
16
|
+
version,
|
|
17
|
+
sharedDomains
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { SUPPORTED_PROTOCOL_VERSION, negotiate };
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_domain_device_id = require("./device-id.cjs");
|
|
3
|
+
const require_domain_handshake = require("./handshake.cjs");
|
|
4
|
+
const require_adapters_frame_codec = require("../adapters/frame-codec.cjs");
|
|
5
|
+
//#region src/domain/mesh-session.ts
|
|
6
|
+
const MS_PER_SECOND = 1e3;
|
|
7
|
+
/** 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. */
|
|
8
|
+
const HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
9
|
+
function localHandshake(domains) {
|
|
10
|
+
return {
|
|
11
|
+
type: "handshake",
|
|
12
|
+
version: 1,
|
|
13
|
+
domains: [...domains]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 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.
|
|
18
|
+
*/
|
|
19
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
20
|
+
let connection = null;
|
|
21
|
+
let state = { status: "idle" };
|
|
22
|
+
let handshake = { status: "pending" };
|
|
23
|
+
const directory = /* @__PURE__ */ new Map();
|
|
24
|
+
const frameLog = [];
|
|
25
|
+
let feedCancelled = false;
|
|
26
|
+
const eventWaiters = [];
|
|
27
|
+
const eventBacklog = [];
|
|
28
|
+
let handshakeTimer = null;
|
|
29
|
+
let reconnectTimer = null;
|
|
30
|
+
let attempt = 0;
|
|
31
|
+
let currentToken = null;
|
|
32
|
+
let nextRequestId = 0;
|
|
33
|
+
let relayPeerDevice = null;
|
|
34
|
+
const pendingManageRequests = /* @__PURE__ */ new Map();
|
|
35
|
+
const incomingWaiters = [];
|
|
36
|
+
const incomingBacklog = [];
|
|
37
|
+
const revocationWaiters = [];
|
|
38
|
+
const revocationBacklog = [];
|
|
39
|
+
function snapshot() {
|
|
40
|
+
return {
|
|
41
|
+
state,
|
|
42
|
+
directory: [...directory.values()],
|
|
43
|
+
frameLog: [...frameLog]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function emit() {
|
|
47
|
+
const event = snapshot();
|
|
48
|
+
const waiter = eventWaiters.shift();
|
|
49
|
+
if (waiter) waiter(event);
|
|
50
|
+
else eventBacklog.push(event);
|
|
51
|
+
}
|
|
52
|
+
function emitIncomingManageRequest(request) {
|
|
53
|
+
const waiter = incomingWaiters.shift();
|
|
54
|
+
if (waiter) waiter(request);
|
|
55
|
+
else incomingBacklog.push(request);
|
|
56
|
+
}
|
|
57
|
+
function emitRevocationEntry(entry) {
|
|
58
|
+
const waiter = revocationWaiters.shift();
|
|
59
|
+
if (waiter) waiter(entry);
|
|
60
|
+
else revocationBacklog.push(entry);
|
|
61
|
+
}
|
|
62
|
+
function rejectPendingManageRequests(reason) {
|
|
63
|
+
for (const pending of pendingManageRequests.values()) pending.reject(new Error(reason));
|
|
64
|
+
pendingManageRequests.clear();
|
|
65
|
+
}
|
|
66
|
+
function buildManageRequest(command, scope, tokenOverride) {
|
|
67
|
+
const requestId = nextRequestId;
|
|
68
|
+
nextRequestId += 1;
|
|
69
|
+
const token = tokenOverride ?? currentToken;
|
|
70
|
+
return {
|
|
71
|
+
type: "manage-request",
|
|
72
|
+
"request-id": requestId,
|
|
73
|
+
command,
|
|
74
|
+
scope,
|
|
75
|
+
...token !== null ? { token } : {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** 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. */
|
|
79
|
+
async function transmit(frame, viaRelay) {
|
|
80
|
+
if (connection === null) throw new Error("not connected");
|
|
81
|
+
if (viaRelay) {
|
|
82
|
+
const relayFrame = {
|
|
83
|
+
type: "relay-data",
|
|
84
|
+
payload: require_adapters_frame_codec.messageFromFrame(frame)
|
|
85
|
+
};
|
|
86
|
+
await connection.send(relayFrame);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await connection.send(frame);
|
|
90
|
+
}
|
|
91
|
+
function applyManageResponse(frame) {
|
|
92
|
+
const requestId = frame["request-id"];
|
|
93
|
+
const pending = pendingManageRequests.get(requestId);
|
|
94
|
+
if (pending !== void 0) {
|
|
95
|
+
pendingManageRequests.delete(requestId);
|
|
96
|
+
pending.resolve(frame.outcome);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function applyManageRequest(frame, viaRelay) {
|
|
100
|
+
const requestId = frame["request-id"];
|
|
101
|
+
const fromDevice = viaRelay && relayPeerDevice !== null ? relayPeerDevice : void 0;
|
|
102
|
+
emitIncomingManageRequest({
|
|
103
|
+
requestId,
|
|
104
|
+
command: frame.command,
|
|
105
|
+
scope: frame.scope,
|
|
106
|
+
...frame.token !== void 0 ? { token: frame.token } : {},
|
|
107
|
+
...fromDevice !== void 0 ? { fromDevice } : {},
|
|
108
|
+
respond: async (outcome) => {
|
|
109
|
+
const response = {
|
|
110
|
+
type: "manage-response",
|
|
111
|
+
"request-id": requestId,
|
|
112
|
+
outcome
|
|
113
|
+
};
|
|
114
|
+
frameLog.push({
|
|
115
|
+
direction: "sent",
|
|
116
|
+
frame: response
|
|
117
|
+
});
|
|
118
|
+
await transmit(response, viaRelay);
|
|
119
|
+
emit();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function applyFrame(frame) {
|
|
124
|
+
if (frame.type === "relay-data") {
|
|
125
|
+
const inner = require_adapters_frame_codec.tryDecodeFrame(frame.payload);
|
|
126
|
+
if (inner !== null && (inner.type === "manage-request" || inner.type === "manage-response")) {
|
|
127
|
+
frameLog.push({
|
|
128
|
+
direction: "received",
|
|
129
|
+
frame: inner
|
|
130
|
+
});
|
|
131
|
+
if (inner.type === "manage-response") applyManageResponse(inner);
|
|
132
|
+
else applyManageRequest(inner, true);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
frameLog.push({
|
|
136
|
+
direction: "received",
|
|
137
|
+
frame
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
frameLog.push({
|
|
142
|
+
direction: "received",
|
|
143
|
+
frame
|
|
144
|
+
});
|
|
145
|
+
if (frame.type === "handshake") applyRemoteHandshake(frame);
|
|
146
|
+
else if (frame.type === "gossip") for (const advert of frame.peers) {
|
|
147
|
+
directory.set(require_domain_device_id.deviceIdToHex(advert.device), {
|
|
148
|
+
device: advert.device,
|
|
149
|
+
advert
|
|
150
|
+
});
|
|
151
|
+
onPeerAdvert?.(advert);
|
|
152
|
+
}
|
|
153
|
+
else if (frame.type === "relay-inbound") relayPeerDevice = frame["source-device"];
|
|
154
|
+
else if (frame.type === "manage-response") applyManageResponse(frame);
|
|
155
|
+
else if (frame.type === "manage-request") applyManageRequest(frame, false);
|
|
156
|
+
else if (frame.type === "revocation-announce") for (const entry of frame.entries) emitRevocationEntry(entry);
|
|
157
|
+
}
|
|
158
|
+
/** 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. */
|
|
159
|
+
async function ensureRelayPairing(targetDevice) {
|
|
160
|
+
if (connection === null) throw new Error("not connected");
|
|
161
|
+
if (relayPeerDevice !== null && require_domain_device_id.deviceIdToHex(relayPeerDevice) === require_domain_device_id.deviceIdToHex(targetDevice)) return;
|
|
162
|
+
const relayConnect = {
|
|
163
|
+
type: "relay-connect",
|
|
164
|
+
"target-device": targetDevice
|
|
165
|
+
};
|
|
166
|
+
frameLog.push({
|
|
167
|
+
direction: "sent",
|
|
168
|
+
frame: relayConnect
|
|
169
|
+
});
|
|
170
|
+
await connection.send(relayConnect);
|
|
171
|
+
relayPeerDevice = targetDevice;
|
|
172
|
+
emit();
|
|
173
|
+
}
|
|
174
|
+
let localHandshakeSent = localHandshake([]);
|
|
175
|
+
function applyRemoteHandshake(remote) {
|
|
176
|
+
if (handshake.status !== "pending") return;
|
|
177
|
+
if (handshakeTimer !== null) {
|
|
178
|
+
clearTimeout(handshakeTimer);
|
|
179
|
+
handshakeTimer = null;
|
|
180
|
+
}
|
|
181
|
+
const result = require_domain_handshake.negotiate(localHandshakeSent, remote);
|
|
182
|
+
handshake = result.ok ? {
|
|
183
|
+
status: "negotiated",
|
|
184
|
+
version: result.version,
|
|
185
|
+
sharedDomains: result.sharedDomains
|
|
186
|
+
} : {
|
|
187
|
+
status: "rejected",
|
|
188
|
+
reason: "no shared domains or version"
|
|
189
|
+
};
|
|
190
|
+
if (state.status === "connected") state = {
|
|
191
|
+
...state,
|
|
192
|
+
handshake
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function handleDisconnect(reason, address, localDomains) {
|
|
196
|
+
if (feedCancelled) return;
|
|
197
|
+
rejectPendingManageRequests("disconnected before a response arrived");
|
|
198
|
+
if (reconnect !== null && attempt < reconnect.maxAttempts) {
|
|
199
|
+
attempt += 1;
|
|
200
|
+
const currentAttempt = attempt;
|
|
201
|
+
state = {
|
|
202
|
+
status: "reconnecting",
|
|
203
|
+
address,
|
|
204
|
+
attempt: currentAttempt,
|
|
205
|
+
reason
|
|
206
|
+
};
|
|
207
|
+
emit();
|
|
208
|
+
reconnectTimer = setTimeout(() => {
|
|
209
|
+
reconnectTimer = null;
|
|
210
|
+
doConnect(address, localDomains).catch((error) => {
|
|
211
|
+
handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
|
|
212
|
+
});
|
|
213
|
+
}, reconnect.delayMs(currentAttempt));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
state = {
|
|
217
|
+
status: "closed",
|
|
218
|
+
address,
|
|
219
|
+
reason
|
|
220
|
+
};
|
|
221
|
+
emit();
|
|
222
|
+
}
|
|
223
|
+
async function consume(link, address, localDomains) {
|
|
224
|
+
for await (const frame of link.receive()) {
|
|
225
|
+
if (feedCancelled) return;
|
|
226
|
+
applyFrame(frame);
|
|
227
|
+
emit();
|
|
228
|
+
}
|
|
229
|
+
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
230
|
+
}
|
|
231
|
+
/** 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. */
|
|
232
|
+
async function wireUpConnection(link, address, localDomains) {
|
|
233
|
+
connection = link;
|
|
234
|
+
localHandshakeSent = localHandshake(localDomains);
|
|
235
|
+
handshake = { status: "pending" };
|
|
236
|
+
state = {
|
|
237
|
+
status: "connected",
|
|
238
|
+
address,
|
|
239
|
+
handshake
|
|
240
|
+
};
|
|
241
|
+
frameLog.push({
|
|
242
|
+
direction: "sent",
|
|
243
|
+
frame: localHandshakeSent
|
|
244
|
+
});
|
|
245
|
+
await connection.send(localHandshakeSent);
|
|
246
|
+
emit();
|
|
247
|
+
const selfAdvert = {
|
|
248
|
+
type: "gossip",
|
|
249
|
+
peers: [{
|
|
250
|
+
device: identity.deviceId,
|
|
251
|
+
addresses: [],
|
|
252
|
+
"snapshot-seconds": Math.floor(clock.now() / MS_PER_SECOND)
|
|
253
|
+
}]
|
|
254
|
+
};
|
|
255
|
+
frameLog.push({
|
|
256
|
+
direction: "sent",
|
|
257
|
+
frame: selfAdvert
|
|
258
|
+
});
|
|
259
|
+
await connection.send(selfAdvert);
|
|
260
|
+
emit();
|
|
261
|
+
handshakeTimer = setTimeout(() => {
|
|
262
|
+
handshakeTimer = null;
|
|
263
|
+
if (handshake.status === "pending") {
|
|
264
|
+
handshake = { status: "unanswered" };
|
|
265
|
+
if (state.status === "connected") state = {
|
|
266
|
+
...state,
|
|
267
|
+
handshake
|
|
268
|
+
};
|
|
269
|
+
emit();
|
|
270
|
+
}
|
|
271
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
272
|
+
consume(connection, address, localDomains).catch((error) => {
|
|
273
|
+
if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async function doConnect(address, localDomains) {
|
|
277
|
+
if (dial === null) throw new Error("this session is already connected; there is nothing to dial");
|
|
278
|
+
if (handshakeTimer !== null) {
|
|
279
|
+
clearTimeout(handshakeTimer);
|
|
280
|
+
handshakeTimer = null;
|
|
281
|
+
}
|
|
282
|
+
state = {
|
|
283
|
+
status: "connecting",
|
|
284
|
+
address
|
|
285
|
+
};
|
|
286
|
+
emit();
|
|
287
|
+
const link = await dial(address);
|
|
288
|
+
if (feedCancelled) {
|
|
289
|
+
await link.close();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
await wireUpConnection(link, address, localDomains);
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
wireUpConnection,
|
|
296
|
+
session: {
|
|
297
|
+
events: { [Symbol.asyncIterator]() {
|
|
298
|
+
return { next: async () => new Promise((resolve) => {
|
|
299
|
+
const backlogEvent = eventBacklog.shift();
|
|
300
|
+
if (backlogEvent) resolve({
|
|
301
|
+
value: backlogEvent,
|
|
302
|
+
done: false
|
|
303
|
+
});
|
|
304
|
+
else eventWaiters.push((event) => {
|
|
305
|
+
resolve({
|
|
306
|
+
value: event,
|
|
307
|
+
done: false
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}) };
|
|
311
|
+
} },
|
|
312
|
+
incomingManageRequests: { [Symbol.asyncIterator]() {
|
|
313
|
+
return { next: async () => new Promise((resolve) => {
|
|
314
|
+
const backlogRequest = incomingBacklog.shift();
|
|
315
|
+
if (backlogRequest) resolve({
|
|
316
|
+
value: backlogRequest,
|
|
317
|
+
done: false
|
|
318
|
+
});
|
|
319
|
+
else incomingWaiters.push((request) => {
|
|
320
|
+
resolve({
|
|
321
|
+
value: request,
|
|
322
|
+
done: false
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}) };
|
|
326
|
+
} },
|
|
327
|
+
revocationAnnouncements: { [Symbol.asyncIterator]() {
|
|
328
|
+
return { next: async () => new Promise((resolve) => {
|
|
329
|
+
const backlogEntry = revocationBacklog.shift();
|
|
330
|
+
if (backlogEntry) resolve({
|
|
331
|
+
value: backlogEntry,
|
|
332
|
+
done: false
|
|
333
|
+
});
|
|
334
|
+
else revocationWaiters.push((entry) => {
|
|
335
|
+
resolve({
|
|
336
|
+
value: entry,
|
|
337
|
+
done: false
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}) };
|
|
341
|
+
} },
|
|
342
|
+
async connect(address, localDomains) {
|
|
343
|
+
if (connection !== null) throw new Error("a session connects once; create a new one to reconnect");
|
|
344
|
+
attempt = 0;
|
|
345
|
+
await doConnect(address, localDomains);
|
|
346
|
+
},
|
|
347
|
+
async sendPing() {
|
|
348
|
+
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
349
|
+
const ping = { type: "ping" };
|
|
350
|
+
frameLog.push({
|
|
351
|
+
direction: "sent",
|
|
352
|
+
frame: ping
|
|
353
|
+
});
|
|
354
|
+
await connection.send(ping);
|
|
355
|
+
emit();
|
|
356
|
+
},
|
|
357
|
+
setToken(token) {
|
|
358
|
+
currentToken = token;
|
|
359
|
+
},
|
|
360
|
+
async sendManageRequest(command, scope, targetDevice, token) {
|
|
361
|
+
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
362
|
+
if (targetDevice !== void 0) await ensureRelayPairing(targetDevice);
|
|
363
|
+
const frame = buildManageRequest(command, scope, token);
|
|
364
|
+
const outcome = new Promise((resolve, reject) => {
|
|
365
|
+
pendingManageRequests.set(frame["request-id"], {
|
|
366
|
+
resolve,
|
|
367
|
+
reject
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
frameLog.push({
|
|
371
|
+
direction: "sent",
|
|
372
|
+
frame
|
|
373
|
+
});
|
|
374
|
+
await transmit(frame, targetDevice !== void 0);
|
|
375
|
+
emit();
|
|
376
|
+
return outcome;
|
|
377
|
+
},
|
|
378
|
+
async sendRevocationAnnounce(entries) {
|
|
379
|
+
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
380
|
+
const frame = {
|
|
381
|
+
type: "revocation-announce",
|
|
382
|
+
entries: [...entries]
|
|
383
|
+
};
|
|
384
|
+
frameLog.push({
|
|
385
|
+
direction: "sent",
|
|
386
|
+
frame
|
|
387
|
+
});
|
|
388
|
+
await transmit(frame, false);
|
|
389
|
+
emit();
|
|
390
|
+
},
|
|
391
|
+
async close() {
|
|
392
|
+
feedCancelled = true;
|
|
393
|
+
if (handshakeTimer !== null) {
|
|
394
|
+
clearTimeout(handshakeTimer);
|
|
395
|
+
handshakeTimer = null;
|
|
396
|
+
}
|
|
397
|
+
if (reconnectTimer !== null) {
|
|
398
|
+
clearTimeout(reconnectTimer);
|
|
399
|
+
reconnectTimer = null;
|
|
400
|
+
}
|
|
401
|
+
rejectPendingManageRequests("connection closed before a response arrived");
|
|
402
|
+
if (connection !== null) await connection.close();
|
|
403
|
+
if (state.status === "connected" || state.status === "connecting" || state.status === "reconnecting") state = {
|
|
404
|
+
status: "closed",
|
|
405
|
+
address: state.address,
|
|
406
|
+
reason: "closed by you"
|
|
407
|
+
};
|
|
408
|
+
emit();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null) {
|
|
414
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address));
|
|
415
|
+
return session;
|
|
416
|
+
}
|
|
417
|
+
/** 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. */
|
|
418
|
+
async function acceptMeshSession(connection, identity, localDomains, options = {}) {
|
|
419
|
+
const clock = options.clock ?? { now: () => Date.now() };
|
|
420
|
+
let resolvePeerDeviceId = null;
|
|
421
|
+
const peerDeviceId = new Promise((resolve) => {
|
|
422
|
+
resolvePeerDeviceId = resolve;
|
|
423
|
+
});
|
|
424
|
+
let peerDeviceIdResolved = false;
|
|
425
|
+
const { session, wireUpConnection } = createSessionCore(identity, clock, null, null, (advert) => {
|
|
426
|
+
if (peerDeviceIdResolved) return;
|
|
427
|
+
peerDeviceIdResolved = true;
|
|
428
|
+
resolvePeerDeviceId?.(advert.device);
|
|
429
|
+
});
|
|
430
|
+
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
431
|
+
return {
|
|
432
|
+
...session,
|
|
433
|
+
peerDeviceId
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
exports.HANDSHAKE_TIMEOUT_MS = HANDSHAKE_TIMEOUT_MS;
|
|
438
|
+
exports.acceptMeshSession = acceptMeshSession;
|
|
439
|
+
exports.createMeshSession = createMeshSession;
|
|
@@ -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.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-BRLEUfVY.cjs";
|
|
3
|
+
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
|
+
import { Connection, Transport } from "../ports/transport.cjs";
|
|
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
|