wire-mesh-core 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/adapters/frame-codec.d.cts +1 -1
- package/dist/adapters/frame-codec.d.mts +1 -1
- package/dist/adapters/node-identity.d.cts +2 -2
- package/dist/adapters/node-identity.d.mts +2 -2
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/threshold-identity.d.cts +2 -2
- package/dist/adapters/threshold-identity.d.mts +2 -2
- package/dist/adapters/threshold-wasm.d.cts +1 -1
- package/dist/adapters/threshold-wasm.d.mts +1 -1
- package/dist/adapters/tls-transport.d.cts +1 -1
- package/dist/adapters/tls-transport.d.mts +1 -1
- package/dist/domain/advert-extension-policy.d.cts +1 -1
- package/dist/domain/advert-extension-policy.d.mts +1 -1
- package/dist/domain/bulk.d.cts +2 -2
- package/dist/domain/bulk.d.mts +2 -2
- package/dist/domain/capability-grant.d.cts +3 -3
- package/dist/domain/capability-grant.d.mts +3 -3
- package/dist/domain/capability-request.d.cts +2 -2
- package/dist/domain/capability-request.d.mts +2 -2
- package/dist/domain/data-sync.d.cts +2 -2
- package/dist/domain/data-sync.d.mts +2 -2
- package/dist/domain/device-id.d.cts +1 -1
- package/dist/domain/device-id.d.mts +1 -1
- package/dist/domain/direct-manage-request.d.cts +2 -2
- package/dist/domain/direct-manage-request.d.mts +2 -2
- package/dist/domain/gossip-expansion.d.cts +1 -1
- package/dist/domain/gossip-expansion.d.mts +1 -1
- package/dist/domain/grant-candidates.d.cts +1 -1
- package/dist/domain/grant-candidates.d.mts +1 -1
- package/dist/domain/handshake.d.cts +1 -1
- package/dist/domain/handshake.d.mts +1 -1
- package/dist/domain/mesh-session.cjs +325 -23
- package/dist/domain/mesh-session.d.cts +4 -4
- package/dist/domain/mesh-session.d.mts +4 -4
- package/dist/domain/mesh-session.mjs +326 -24
- package/dist/domain/notice-board.d.cts +3 -3
- package/dist/domain/notice-board.d.mts +3 -3
- package/dist/domain/path-trace.d.cts +1 -1
- package/dist/domain/path-trace.d.mts +1 -1
- package/dist/domain/peer-advert.d.cts +2 -2
- package/dist/domain/peer-advert.d.mts +2 -2
- package/dist/domain/relay-hub.d.cts +2 -2
- package/dist/domain/relay-hub.d.mts +2 -2
- package/dist/domain/relay-use-gate.d.cts +2 -2
- package/dist/domain/relay-use-gate.d.mts +2 -2
- package/dist/domain/revocation-view.d.cts +2 -2
- package/dist/domain/revocation-view.d.mts +2 -2
- package/dist/domain/room-rekey.d.cts +3 -3
- package/dist/domain/room-rekey.d.mts +3 -3
- package/dist/domain/room-token-verification.d.cts +1 -1
- package/dist/domain/room-token-verification.d.mts +1 -1
- package/dist/domain/room.d.cts +4 -4
- package/dist/domain/room.d.mts +4 -4
- package/dist/domain/shard-manifest.d.cts +1 -1
- package/dist/domain/shard-manifest.d.mts +1 -1
- package/dist/domain/tokens.d.cts +1 -1
- package/dist/domain/tokens.d.mts +1 -1
- package/dist/domain/topology-snapshot.d.cts +1 -1
- package/dist/domain/topology-snapshot.d.mts +1 -1
- package/dist/domain/topology.d.cts +1 -1
- package/dist/domain/topology.d.mts +1 -1
- package/dist/domain/webrtc-signaling.d.cts +2 -2
- package/dist/domain/webrtc-signaling.d.mts +2 -2
- package/dist/generated/protocol.cjs +18 -1
- package/dist/generated/protocol.d.cts +2 -2
- package/dist/generated/protocol.d.mts +2 -2
- package/dist/generated/protocol.mjs +17 -2
- package/dist/{identity-DImeXZcq.d.cts → identity-DVCNuFLG.d.cts} +1 -1
- package/dist/{identity-Dc4xmwp3.d.mts → identity-DcaYgjXT.d.mts} +1 -1
- package/dist/ports/identity.d.cts +1 -1
- package/dist/ports/identity.d.mts +1 -1
- package/dist/ports/transport.d.cts +1 -1
- package/dist/ports/transport.d.mts +1 -1
- package/dist/{protocol-Bi2fzBCc.d.cts → protocol-DCDae3z4.d.cts} +47 -1
- package/dist/{protocol-Bi2fzBCc.d.mts → protocol-DCDae3z4.d.mts} +47 -1
- package/dist/{room-token-verification-CDXkm_Tg.d.cts → room-token-verification-CkGnFuQc.d.mts} +2 -2
- package/dist/{room-token-verification-CnHNaA8J.d.mts → room-token-verification-PSv4eQ1x.d.cts} +2 -2
- package/dist/{tokens-0r9ipK1r.d.mts → tokens-CBiSYuzp.d.mts} +2 -2
- package/dist/{tokens-BDVxEvl2.d.cts → tokens-DqFu7iMX.d.cts} +2 -2
- package/dist/{transport-Cq_odHy3.d.cts → transport-CfW7fsGK.d.cts} +1 -1
- package/dist/{transport-GcJ4LYVE.d.mts → transport-iLBxIkRS.d.mts} +1 -1
- package/package.json +1 -1
- package/wasm-dist/wire_mesh_threshold_wasm_bg.wasm +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_gossip_extensions = require("../gossip-extensions-BBYgMZ2N.cjs");
|
|
3
|
+
const require_token_scope = require("../token-scope-CxHXTT3u.cjs");
|
|
3
4
|
const require_domain_peer_advert = require("./peer-advert.cjs");
|
|
4
5
|
const require_domain_device_id = require("./device-id.cjs");
|
|
5
6
|
const require_domain_handshake = require("./handshake.cjs");
|
|
@@ -22,6 +23,260 @@ function createRelayPairings() {
|
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
//#endregion
|
|
26
|
+
//#region src/domain/secure-channel.ts
|
|
27
|
+
/**
|
|
28
|
+
* The end-to-end channel two devices open between themselves inside a relay pairing (spec/secure-channel.cddl). A relay only forwards the bytes of a `relay-data` frame, and without this it could read them, alter them, and say who sent them. Here the two devices authenticate each other's identity keys over a fresh ECDH exchange, derive one AES-256-GCM key per direction, and seal every frame they exchange so the relay sees ciphertext and every receiver learns the sender's identity from the handshake and nothing a relay asserts.
|
|
29
|
+
*
|
|
30
|
+
* Pure crypto and framing, with no connection or session in it: beginHandshake and verifyHello and establishChannel are the three steps of the handshake, and a SecureChannel then seals and opens frames. mesh-session.ts owns when to do each, and where the results travel.
|
|
31
|
+
*/
|
|
32
|
+
const NONCE_BYTE_LENGTH = 16;
|
|
33
|
+
const AES_KEY_BIT_LENGTH = 256;
|
|
34
|
+
const ECDH_SECRET_BIT_LENGTH = 256;
|
|
35
|
+
const GCM_IV_BYTE_LENGTH = 12;
|
|
36
|
+
/** The counter fills the last eight bytes of the IV; the four before it stay zero. */
|
|
37
|
+
const IV_COUNTER_OFFSET = 4;
|
|
38
|
+
const COUNTER_BYTE_LENGTH = 8;
|
|
39
|
+
const encoder = new TextEncoder();
|
|
40
|
+
const HELLO_CONTEXT = encoder.encode("wire-mesh/secure-hello/v1\0");
|
|
41
|
+
const CHANNEL_CONTEXT = encoder.encode("wire-mesh/secure-channel/v1\0");
|
|
42
|
+
const DATA_CONTEXT = encoder.encode("wire-mesh/secure-data/v1\0");
|
|
43
|
+
const LABEL_LOW_TO_HIGH = encoder.encode("lo-to-hi");
|
|
44
|
+
const LABEL_HIGH_TO_LOW = encoder.encode("hi-to-lo");
|
|
45
|
+
const ECDH_PARAMS = {
|
|
46
|
+
name: "ECDH",
|
|
47
|
+
namedCurve: "P-256"
|
|
48
|
+
};
|
|
49
|
+
function concat(...parts) {
|
|
50
|
+
const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
|
|
51
|
+
let offset = 0;
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
out.set(part, offset);
|
|
54
|
+
offset += part.length;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** Bytewise order of two device-ids: negative when a sorts first. */
|
|
59
|
+
function compareBytes(a, b) {
|
|
60
|
+
for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
|
|
61
|
+
const difference = (a[i] ?? 0) - (b[i] ?? 0);
|
|
62
|
+
if (difference !== 0) return difference;
|
|
63
|
+
}
|
|
64
|
+
return a.length - b.length;
|
|
65
|
+
}
|
|
66
|
+
function counterBytes(counter) {
|
|
67
|
+
const bytes = new Uint8Array(COUNTER_BYTE_LENGTH);
|
|
68
|
+
new DataView(bytes.buffer).setBigUint64(0, BigInt(counter), false);
|
|
69
|
+
return bytes;
|
|
70
|
+
}
|
|
71
|
+
/** Exactly what a hello's signature covers: see spec/secure-channel.cddl. */
|
|
72
|
+
function helloSigningInput(ephemeralKey, nonce, sender, recipient) {
|
|
73
|
+
return concat(HELLO_CONTEXT, ephemeralKey, nonce, sender, recipient);
|
|
74
|
+
}
|
|
75
|
+
/** Generates this side's ephemeral key pair and nonce and signs a hello for peer. */
|
|
76
|
+
async function beginHandshake(identity, peer) {
|
|
77
|
+
const pair = await crypto.subtle.generateKey(ECDH_PARAMS, false, ["deriveBits"]);
|
|
78
|
+
const ephemeralKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
79
|
+
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTE_LENGTH));
|
|
80
|
+
const signature = await identity.sign(helloSigningInput(ephemeralKey, nonce, identity.deviceId, peer));
|
|
81
|
+
return {
|
|
82
|
+
peer,
|
|
83
|
+
privateKey: pair.privateKey,
|
|
84
|
+
hello: {
|
|
85
|
+
type: "secure-hello",
|
|
86
|
+
"ephemeral-key": ephemeralKey,
|
|
87
|
+
nonce,
|
|
88
|
+
"identity-key": identity.identityKey,
|
|
89
|
+
"to-device": peer,
|
|
90
|
+
signature
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Checks a received hello: its identity key must hash to a device-id (which is then the peer), it must be addressed to this device, and its signature must verify under that key over the input spec/secure-channel.cddl defines. Never throws: a hostile hello gets a verdict.
|
|
96
|
+
*/
|
|
97
|
+
async function verifyHello(identity, hello) {
|
|
98
|
+
const key = hello["identity-key"];
|
|
99
|
+
try {
|
|
100
|
+
const sender = await identity.deriveDeviceId(key["public-key"]);
|
|
101
|
+
if (!require_token_scope.bytesEqual(hello["to-device"], identity.deviceId)) return {
|
|
102
|
+
ok: false,
|
|
103
|
+
reason: "wrong_recipient"
|
|
104
|
+
};
|
|
105
|
+
return await identity.verify(key, helloSigningInput(hello["ephemeral-key"], hello.nonce, sender, identity.deviceId), hello.signature) ? {
|
|
106
|
+
ok: true,
|
|
107
|
+
peer: {
|
|
108
|
+
deviceId: sender,
|
|
109
|
+
identityKey: key
|
|
110
|
+
}
|
|
111
|
+
} : {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: "bad_signature"
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
reason: "identity_mismatch"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function ivFor(counter) {
|
|
123
|
+
const iv = new Uint8Array(GCM_IV_BYTE_LENGTH);
|
|
124
|
+
iv.set(counterBytes(counter), IV_COUNTER_OFFSET);
|
|
125
|
+
return iv;
|
|
126
|
+
}
|
|
127
|
+
function associatedData(sender, recipient, counter) {
|
|
128
|
+
return concat(DATA_CONTEXT, sender, recipient, counterBytes(counter));
|
|
129
|
+
}
|
|
130
|
+
async function deriveDirectionKey(secret, salt, low, high, label) {
|
|
131
|
+
const material = await crypto.subtle.importKey("raw", Uint8Array.from(secret), "HKDF", false, ["deriveKey"]);
|
|
132
|
+
return crypto.subtle.deriveKey({
|
|
133
|
+
name: "HKDF",
|
|
134
|
+
hash: "SHA-256",
|
|
135
|
+
salt: Uint8Array.from(salt),
|
|
136
|
+
info: concat(CHANNEL_CONTEXT, low, high, label)
|
|
137
|
+
}, material, {
|
|
138
|
+
name: "AES-GCM",
|
|
139
|
+
length: AES_KEY_BIT_LENGTH
|
|
140
|
+
}, false, ["encrypt", "decrypt"]);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Completes a handshake once the peer's hello has verified: derives the two directional keys from the ECDH result of the ephemeral keys and returns the channel. Returns undefined when the peer's ephemeral key is not a valid P-256 point, which only a hostile or broken peer sends.
|
|
144
|
+
*
|
|
145
|
+
* `peer` is the device verifyHello authenticated for `remote`, and `pending` is this side's own half, whether the handshake was begun here or in answer to that hello.
|
|
146
|
+
*/
|
|
147
|
+
async function establishChannel(identity, pending, remote, peer) {
|
|
148
|
+
let secret;
|
|
149
|
+
try {
|
|
150
|
+
const remoteKey = await crypto.subtle.importKey("raw", Uint8Array.from(remote["ephemeral-key"]), ECDH_PARAMS, false, []);
|
|
151
|
+
secret = new Uint8Array(await crypto.subtle.deriveBits({
|
|
152
|
+
name: "ECDH",
|
|
153
|
+
public: remoteKey
|
|
154
|
+
}, pending.privateKey, ECDH_SECRET_BIT_LENGTH));
|
|
155
|
+
} catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const localIsLow = compareBytes(identity.deviceId, peer.deviceId) < 0;
|
|
159
|
+
const low = localIsLow ? identity.deviceId : peer.deviceId;
|
|
160
|
+
const high = localIsLow ? peer.deviceId : identity.deviceId;
|
|
161
|
+
const salt = concat(localIsLow ? pending.hello.nonce : remote.nonce, localIsLow ? remote.nonce : pending.hello.nonce);
|
|
162
|
+
const lowToHigh = await deriveDirectionKey(secret, salt, low, high, LABEL_LOW_TO_HIGH);
|
|
163
|
+
const highToLow = await deriveDirectionKey(secret, salt, low, high, LABEL_HIGH_TO_LOW);
|
|
164
|
+
const sendKey = localIsLow ? lowToHigh : highToLow;
|
|
165
|
+
const receiveKey = localIsLow ? highToLow : lowToHigh;
|
|
166
|
+
let nextSendCounter = 0;
|
|
167
|
+
let highestReceived = -1;
|
|
168
|
+
let sealing = Promise.resolve();
|
|
169
|
+
let opening = Promise.resolve();
|
|
170
|
+
async function sealAt(frame, counter) {
|
|
171
|
+
return {
|
|
172
|
+
type: "secure-data",
|
|
173
|
+
counter,
|
|
174
|
+
ciphertext: new Uint8Array(await crypto.subtle.encrypt({
|
|
175
|
+
name: "AES-GCM",
|
|
176
|
+
iv: ivFor(counter),
|
|
177
|
+
additionalData: associatedData(identity.deviceId, peer.deviceId, counter)
|
|
178
|
+
}, sendKey, require_adapters_frame_codec.messageFromFrame(frame)))
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function openOne(data) {
|
|
182
|
+
if (data.counter <= highestReceived) return void 0;
|
|
183
|
+
let plaintext;
|
|
184
|
+
try {
|
|
185
|
+
plaintext = new Uint8Array(await crypto.subtle.decrypt({
|
|
186
|
+
name: "AES-GCM",
|
|
187
|
+
iv: ivFor(data.counter),
|
|
188
|
+
additionalData: associatedData(peer.deviceId, identity.deviceId, data.counter)
|
|
189
|
+
}, receiveKey, Uint8Array.from(data.ciphertext)));
|
|
190
|
+
} catch {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
highestReceived = data.counter;
|
|
194
|
+
return require_adapters_frame_codec.tryDecodeFrame(plaintext) ?? void 0;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
peer,
|
|
198
|
+
seal: async (frame) => {
|
|
199
|
+
const counter = nextSendCounter;
|
|
200
|
+
nextSendCounter += 1;
|
|
201
|
+
const sealed = sealing.then(async () => sealAt(frame, counter));
|
|
202
|
+
sealing = sealed.catch(() => void 0);
|
|
203
|
+
return sealed;
|
|
204
|
+
},
|
|
205
|
+
open: async (data) => {
|
|
206
|
+
const opened = opening.then(async () => openOne(data));
|
|
207
|
+
opening = opened.catch(() => void 0);
|
|
208
|
+
return opened;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/domain/relay-channels.ts
|
|
214
|
+
function createRelayChannels(identity, sendHello) {
|
|
215
|
+
const channels = /* @__PURE__ */ new Map();
|
|
216
|
+
/** Registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second, then sends this side's hello. */
|
|
217
|
+
function start(peer) {
|
|
218
|
+
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
219
|
+
const settle = Promise.withResolvers();
|
|
220
|
+
const pending = beginHandshake(identity, peer);
|
|
221
|
+
const entry = {
|
|
222
|
+
status: "awaiting",
|
|
223
|
+
pending,
|
|
224
|
+
settle
|
|
225
|
+
};
|
|
226
|
+
channels.set(key, entry);
|
|
227
|
+
pending.then(async ({ hello }) => sendHello(peer, hello)).catch((error) => {
|
|
228
|
+
settle.reject(error instanceof Error ? error : new Error(String(error)));
|
|
229
|
+
if (channels.get(key) === entry) channels.delete(key);
|
|
230
|
+
});
|
|
231
|
+
return entry;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
ensure: async (peer) => {
|
|
235
|
+
const existing = channels.get(require_domain_device_id.deviceIdToHex(peer));
|
|
236
|
+
if (existing?.status === "established") return existing.channel;
|
|
237
|
+
return (existing ?? start(peer)).settle.promise;
|
|
238
|
+
},
|
|
239
|
+
applyHello: async (hello) => {
|
|
240
|
+
const verdict = await verifyHello(identity, hello);
|
|
241
|
+
if (!verdict.ok) return;
|
|
242
|
+
const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
|
|
243
|
+
const current = channels.get(key);
|
|
244
|
+
const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
|
|
245
|
+
const channel = await establishChannel(identity, await entry.pending, hello, verdict.peer);
|
|
246
|
+
if (channel === void 0) {
|
|
247
|
+
entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
248
|
+
if (channels.get(key) === entry) channels.delete(key);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
channels.set(key, {
|
|
252
|
+
status: "established",
|
|
253
|
+
channel
|
|
254
|
+
});
|
|
255
|
+
entry.settle.resolve(channel);
|
|
256
|
+
},
|
|
257
|
+
open: async (data, claimedFrom) => {
|
|
258
|
+
if (claimedFrom === void 0) return;
|
|
259
|
+
const entry = channels.get(require_domain_device_id.deviceIdToHex(claimedFrom));
|
|
260
|
+
if (entry?.status !== "established") return;
|
|
261
|
+
const frame = await entry.channel.open(data);
|
|
262
|
+
return frame === void 0 ? void 0 : {
|
|
263
|
+
frame,
|
|
264
|
+
from: entry.channel.peer.deviceId
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
reset: (reason) => {
|
|
268
|
+
for (const entry of channels.values()) if (entry.status === "awaiting") entry.settle.reject(new Error(reason));
|
|
269
|
+
channels.clear();
|
|
270
|
+
},
|
|
271
|
+
forget: (peer) => {
|
|
272
|
+
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
273
|
+
const entry = channels.get(key);
|
|
274
|
+
if (entry?.status === "awaiting") entry.settle.reject(/* @__PURE__ */ new Error("the relay pairing was lost"));
|
|
275
|
+
channels.delete(key);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
25
280
|
//#region src/domain/mesh-session.ts
|
|
26
281
|
const MS_PER_SECOND = 1e3;
|
|
27
282
|
/** 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. */
|
|
@@ -56,6 +311,15 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
56
311
|
getRelayedDevices: () => relayPairings.list()
|
|
57
312
|
});
|
|
58
313
|
const pendingManageRequests = /* @__PURE__ */ new Map();
|
|
314
|
+
const relayChannels = createRelayChannels(identity, async (peer, hello) => {
|
|
315
|
+
if (connection === null) throw new Error("not connected");
|
|
316
|
+
frameLog.push({
|
|
317
|
+
direction: "sent",
|
|
318
|
+
frame: hello
|
|
319
|
+
});
|
|
320
|
+
await connection.send(require_adapters_frame_codec.wrapRelayData(hello, peer));
|
|
321
|
+
emit();
|
|
322
|
+
});
|
|
59
323
|
const incomingQueue = require_domain_async_queue.createAsyncQueue();
|
|
60
324
|
const revocationQueue = require_domain_async_queue.createAsyncQueue();
|
|
61
325
|
const pingRoundTrips = require_domain_ping_round_trips.createPingRoundTrips();
|
|
@@ -90,28 +354,32 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
90
354
|
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
91
355
|
return connection;
|
|
92
356
|
}
|
|
93
|
-
/** Sends a frame
|
|
357
|
+
/** Sends a frame. Through a relay it is sealed for toDevice on their secure channel first (spec/secure-channel.cddl) and only then wrapped as relay-data, so the hub carries ciphertext. This is the single choke point every outbound manage-request and manage-response passes through, so a consumer of sendManageRequest or respond never needs its own relay logic. */
|
|
94
358
|
async function transmit(frame, viaRelay, toDevice) {
|
|
95
359
|
if (connection === null) throw new Error("not connected");
|
|
96
|
-
if (viaRelay) {
|
|
97
|
-
await connection.send(
|
|
360
|
+
if (!viaRelay) {
|
|
361
|
+
await connection.send(frame);
|
|
98
362
|
return;
|
|
99
363
|
}
|
|
100
|
-
|
|
364
|
+
if (toDevice === void 0) throw new Error("a relayed frame needs the device it is for");
|
|
365
|
+
const sealed = await (await relayChannels.ensure(toDevice)).seal(frame);
|
|
366
|
+
await connection.send(require_adapters_frame_codec.wrapRelayData(sealed, toDevice));
|
|
101
367
|
}
|
|
102
|
-
|
|
368
|
+
/** `from` is the device whose secure channel the response arrived through, absent for one that arrived directly. A pending request only accepts an answer from where it was sent: a peer must not be able to answer a request meant for another by guessing its request-id. */
|
|
369
|
+
function applyManageResponse(frame, from) {
|
|
103
370
|
const requestId = frame["request-id"];
|
|
104
371
|
const pending = pendingManageRequests.get(requestId);
|
|
105
|
-
if (pending
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
372
|
+
if (pending === void 0) return;
|
|
373
|
+
const expected = pending.expectedFrom;
|
|
374
|
+
if (!(expected === void 0 ? from === void 0 : from !== void 0 && require_domain_device_id.deviceIdToHex(from) === require_domain_device_id.deviceIdToHex(expected))) return;
|
|
375
|
+
pendingManageRequests.delete(requestId);
|
|
376
|
+
pending.resolve(frame.outcome);
|
|
109
377
|
}
|
|
110
|
-
/**
|
|
111
|
-
function applyManageRequest(frame,
|
|
378
|
+
/** `origin` is present only for a manage-request that arrived through a relay pairing: who sent it, as their secure channel authenticated them, and the device the relay-data frame was addressed to, if it named one. See IncomingManageRequest's own fromDevice/toDevice doc comments for what each means and why neither is ever guessed from pairing state. */
|
|
379
|
+
function applyManageRequest(frame, origin) {
|
|
112
380
|
const requestId = frame["request-id"];
|
|
113
|
-
const fromDevice =
|
|
114
|
-
const toDevice =
|
|
381
|
+
const fromDevice = origin?.fromDevice;
|
|
382
|
+
const toDevice = origin?.toDevice;
|
|
115
383
|
const incoming = {
|
|
116
384
|
requestId,
|
|
117
385
|
command: frame.command,
|
|
@@ -129,7 +397,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
129
397
|
direction: "sent",
|
|
130
398
|
frame: response
|
|
131
399
|
});
|
|
132
|
-
await transmit(response,
|
|
400
|
+
await transmit(response, origin !== void 0, fromDevice);
|
|
133
401
|
emit();
|
|
134
402
|
}
|
|
135
403
|
};
|
|
@@ -142,13 +410,32 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
142
410
|
async function applyFrame(frame) {
|
|
143
411
|
if (frame.type === "relay-data") {
|
|
144
412
|
const inner = require_adapters_frame_codec.tryDecodeFrame(frame.payload);
|
|
145
|
-
if (inner
|
|
413
|
+
if (inner?.type === "secure-hello") {
|
|
146
414
|
frameLog.push({
|
|
147
415
|
direction: "received",
|
|
148
416
|
frame: inner
|
|
149
417
|
});
|
|
150
|
-
|
|
151
|
-
|
|
418
|
+
await relayChannels.applyHello(inner);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (inner?.type === "secure-data") {
|
|
422
|
+
const opened = await relayChannels.open(inner, frame["from-device"]);
|
|
423
|
+
if (opened?.frame.type === "manage-response") {
|
|
424
|
+
frameLog.push({
|
|
425
|
+
direction: "received",
|
|
426
|
+
frame: opened.frame
|
|
427
|
+
});
|
|
428
|
+
applyManageResponse(opened.frame, opened.from);
|
|
429
|
+
} else if (opened?.frame.type === "manage-request") {
|
|
430
|
+
frameLog.push({
|
|
431
|
+
direction: "received",
|
|
432
|
+
frame: opened.frame
|
|
433
|
+
});
|
|
434
|
+
applyManageRequest(opened.frame, {
|
|
435
|
+
fromDevice: opened.from,
|
|
436
|
+
...frame["to-device"] !== void 0 ? { toDevice: frame["to-device"] } : {}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
152
439
|
return;
|
|
153
440
|
}
|
|
154
441
|
frameLog.push({
|
|
@@ -272,6 +559,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
272
559
|
/** 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. */
|
|
273
560
|
async function wireUpConnection(link, address, localDomains) {
|
|
274
561
|
connection = link;
|
|
562
|
+
relayChannels.reset("the connection changed before the channel was ready");
|
|
275
563
|
localHandshakeSent = localHandshake(localDomains);
|
|
276
564
|
handshake = { status: "pending" };
|
|
277
565
|
state = {
|
|
@@ -369,20 +657,33 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
369
657
|
const outcome = new Promise((resolve, reject) => {
|
|
370
658
|
pendingManageRequests.set(requestId, {
|
|
371
659
|
resolve,
|
|
372
|
-
reject
|
|
660
|
+
reject,
|
|
661
|
+
expectedFrom: targetDevice
|
|
373
662
|
});
|
|
374
663
|
});
|
|
375
664
|
frameLog.push({
|
|
376
665
|
direction: "sent",
|
|
377
666
|
frame
|
|
378
667
|
});
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
668
|
+
const exchange = (async () => {
|
|
669
|
+
try {
|
|
670
|
+
await transmit(frame, targetDevice !== void 0, targetDevice);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
pendingManageRequests.delete(requestId);
|
|
673
|
+
outcome.catch(() => void 0);
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
emit();
|
|
677
|
+
return outcome;
|
|
678
|
+
})();
|
|
679
|
+
if (timeoutMs === void 0) return exchange;
|
|
680
|
+
return Promise.race([exchange, new Promise((resolve) => {
|
|
383
681
|
setTimeout(() => {
|
|
384
682
|
if (pendingManageRequests.delete(requestId)) {
|
|
385
|
-
if (targetDevice !== void 0)
|
|
683
|
+
if (targetDevice !== void 0) {
|
|
684
|
+
relayPairings.remove(targetDevice);
|
|
685
|
+
relayChannels.forget(targetDevice);
|
|
686
|
+
}
|
|
386
687
|
resolve({
|
|
387
688
|
result: "error",
|
|
388
689
|
code: "timeout"
|
|
@@ -437,6 +738,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
437
738
|
reconnectTimer = null;
|
|
438
739
|
}
|
|
439
740
|
rejectPendingManageRequests("connection closed before a response arrived");
|
|
741
|
+
relayChannels.reset("the session closed before the channel was ready");
|
|
440
742
|
pingRoundTrips.rejectAll("connection closed before a pong arrived");
|
|
441
743
|
if (connection !== null) await connection.close();
|
|
442
744
|
if (state.status === "connected" || state.status === "connecting" || state.status === "reconnecting") state = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry,
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-DVCNuFLG.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
4
|
+
import { r as Transport, t as Connection } from "../transport-CfW7fsGK.cjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
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
7
|
export declare const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
@@ -38,7 +38,7 @@ export interface IncomingManageRequest {
|
|
|
38
38
|
command: ManageCommand;
|
|
39
39
|
scope: CapabilityScope;
|
|
40
40
|
token?: CapabilityToken;
|
|
41
|
-
/** The device-id of the peer this request was relayed on behalf of,
|
|
41
|
+
/** The device-id of the peer this request was relayed on behalf of, as the end-to-end secure channel it arrived through authenticated it (spec/secure-channel.cddl): the identity that channel's handshake proved, never the `from-device` a hub stamps on relay-data, which a hub can set to anything. Present only when the request arrived through a relay pairing. 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
42
|
fromDevice?: DeviceId;
|
|
43
43
|
/** The device-id this request's relay-data frame was explicitly addressed to, read from its own `to-device` field -- present only when the request arrived relay-wrapped and the frame carried one. A caller fronting more than one locally-addressable device behind a single hub connection (a gateway advertising several local peers through the same relay pairing) uses this to decide whether the request is for this device or should be routed on to a different local peer it also advertises; this session has no such routing logic of its own, since it represents exactly one identity. */
|
|
44
44
|
toDevice?: DeviceId;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry,
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-DcaYgjXT.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
4
|
+
import { r as Transport, t as Connection } from "../transport-iLBxIkRS.mjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
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
7
|
export declare const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
@@ -38,7 +38,7 @@ export interface IncomingManageRequest {
|
|
|
38
38
|
command: ManageCommand;
|
|
39
39
|
scope: CapabilityScope;
|
|
40
40
|
token?: CapabilityToken;
|
|
41
|
-
/** The device-id of the peer this request was relayed on behalf of,
|
|
41
|
+
/** The device-id of the peer this request was relayed on behalf of, as the end-to-end secure channel it arrived through authenticated it (spec/secure-channel.cddl): the identity that channel's handshake proved, never the `from-device` a hub stamps on relay-data, which a hub can set to anything. Present only when the request arrived through a relay pairing. 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
42
|
fromDevice?: DeviceId;
|
|
43
43
|
/** The device-id this request's relay-data frame was explicitly addressed to, read from its own `to-device` field -- present only when the request arrived relay-wrapped and the frame carried one. A caller fronting more than one locally-addressable device behind a single hub connection (a gateway advertising several local peers through the same relay pairing) uses this to decide whether the request is for this device or should be routed on to a different local peer it also advertises; this session has no such routing logic of its own, since it represents exactly one identity. */
|
|
44
44
|
toDevice?: DeviceId;
|