wire-mesh-core 4.2.0 → 4.2.1
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 -1
- package/dist/domain/bulk.d.cts +1 -1
- package/dist/domain/bulk.d.mts +1 -1
- package/dist/domain/capability-grant.d.cts +1 -1
- package/dist/domain/capability-grant.d.mts +1 -1
- package/dist/domain/capability-request.d.cts +1 -1
- package/dist/domain/capability-request.d.mts +1 -1
- package/dist/domain/consume-incoming.d.cts +1 -1
- package/dist/domain/consume-incoming.d.mts +1 -1
- package/dist/domain/direct-manage-request.d.cts +1 -1
- package/dist/domain/direct-manage-request.d.mts +1 -1
- 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/mesh-session.cjs +366 -115
- package/dist/domain/mesh-session.d.cts +2 -162
- package/dist/domain/mesh-session.d.mts +2 -162
- package/dist/domain/mesh-session.mjs +367 -116
- package/dist/domain/path-trace.d.cts +1 -1
- package/dist/domain/path-trace.d.mts +1 -1
- package/dist/domain/room-rekey.d.cts +1 -1
- package/dist/domain/room-rekey.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 +1 -1
- package/dist/domain/webrtc-signaling.d.mts +1 -1
- package/dist/mesh-session-By06Mnu-.d.cts +174 -0
- package/dist/mesh-session-DzcZYdhe.d.mts +174 -0
- package/package.json +3 -2
|
@@ -6,7 +6,7 @@ import { a as OWN_VERSION, i as CORE_VERSION_GOSSIP_KEY, n as TOPOLOGY_PEERS_GOS
|
|
|
6
6
|
import { signPeerAdvert, verifyPeerAdvert } from "./peer-advert.mjs";
|
|
7
7
|
import { createAsyncQueue } from "./async-queue.mjs";
|
|
8
8
|
import { ChannelExhaustedError } from "./channel-exhausted-error.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { createFaultReporter } from "./fault-report.mjs";
|
|
10
10
|
import { localHandshake, negotiate } from "./handshake.mjs";
|
|
11
11
|
import { isDataFrame } from "./hub-mailbox.mjs";
|
|
12
12
|
import { createPingRoundTrips } from "./ping-round-trips.mjs";
|
|
@@ -139,6 +139,15 @@ async function verifyHello(identity, hello) {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/** True when `key` is a usable P-256 ECDH public key, so a channel can be built from a hello carrying it. Cheap enough to run before any key of this side's own is generated. */
|
|
143
|
+
async function isUsableEphemeralKey(key) {
|
|
144
|
+
try {
|
|
145
|
+
await crypto.subtle.importKey("raw", Uint8Array.from(key), ECDH_PARAMS, false, []);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
142
151
|
/** The largest counter a channel uses: the counter is an unsigned 64-bit integer on the wire, and this implementation holds it as a JavaScript number, exact up to this value. A frame above it is refused at decoding, never truncated, and sealing refuses to go past it, never wraps. */
|
|
143
152
|
const MAX_COUNTER = Number.MAX_SAFE_INTEGER;
|
|
144
153
|
/** Throws a RangeError unless `counter` is a counter a channel can start from: a non-negative safe integer. */
|
|
@@ -277,140 +286,382 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
|
|
|
277
286
|
}
|
|
278
287
|
//#endregion
|
|
279
288
|
//#region src/domain/relay-channels.ts
|
|
289
|
+
/** How long a peer may take to answer a hello, and the window every other rule here is measured in: a handshake is outstanding for this long, a replaced channel may still open frames that were in flight for this long, and an entry younger than this is never reused. Whoever holds a hello for longer than this gains nothing, since the handshake it belongs to is over. */
|
|
290
|
+
const HANDSHAKE_WINDOW_MS = 3e3;
|
|
291
|
+
/** Everything held for one peer. The fields only change through the methods, so each transition has one place. */
|
|
292
|
+
var PeerChannels = class {
|
|
293
|
+
peer;
|
|
294
|
+
chosen = false;
|
|
295
|
+
lastUsed;
|
|
296
|
+
/** The current channel: sealing goes through it and only it. */
|
|
297
|
+
current;
|
|
298
|
+
/** The channel the current one replaced, which may still open frames that were in flight but never seals. */
|
|
299
|
+
replaced;
|
|
300
|
+
hs;
|
|
301
|
+
/** The candidates of a handshake that ended without confirming either of two, which together serve as the channel until a frame opens under one. */
|
|
302
|
+
pool = [];
|
|
303
|
+
waiters;
|
|
304
|
+
opening = Promise.resolve();
|
|
305
|
+
constructor(peer, now) {
|
|
306
|
+
this.peer = peer;
|
|
307
|
+
this.lastUsed = now;
|
|
308
|
+
}
|
|
309
|
+
get key() {
|
|
310
|
+
return deviceIdToHex(this.peer);
|
|
311
|
+
}
|
|
312
|
+
touch(now) {
|
|
313
|
+
this.lastUsed = now;
|
|
314
|
+
}
|
|
315
|
+
choose() {
|
|
316
|
+
this.chosen = true;
|
|
317
|
+
}
|
|
318
|
+
/** True when `key` is the remote ephemeral key of anything held for the peer. */
|
|
319
|
+
holdsKey(key) {
|
|
320
|
+
return [
|
|
321
|
+
this.current?.remoteKey,
|
|
322
|
+
this.replaced?.remoteKey,
|
|
323
|
+
...(this.hs?.cands ?? []).map((cand) => cand.remoteKey),
|
|
324
|
+
...this.pool.map((cand) => cand.remoteKey)
|
|
325
|
+
].some((held) => held !== void 0 && bytesEqual(held, key));
|
|
326
|
+
}
|
|
327
|
+
/** Whether this side's handshake with the peer is outstanding: sent less than a window ago, and neither confirmed nor ended. */
|
|
328
|
+
outstanding(now) {
|
|
329
|
+
return this.hs !== void 0 && !this.hs.expired && now < this.hs.sentAt + 3e3;
|
|
330
|
+
}
|
|
331
|
+
/** Applies whatever the passing of time has ended: the replaced channel after its window, and a handshake after its window. Checked on every event, so no timer is needed. */
|
|
332
|
+
expire(now) {
|
|
333
|
+
if (this.replaced !== void 0 && now >= this.replaced.until) this.replaced = void 0;
|
|
334
|
+
const { hs } = this;
|
|
335
|
+
if (hs === void 0 || hs.expired || now < hs.sentAt + 3e3) return;
|
|
336
|
+
const [only, ...rest] = hs.cands;
|
|
337
|
+
if (this.current !== void 0) this.hs = void 0;
|
|
338
|
+
else if (only === void 0) hs.expired = true;
|
|
339
|
+
else if (rest.length === 0) {
|
|
340
|
+
this.current = {
|
|
341
|
+
...only,
|
|
342
|
+
confirmed: false
|
|
343
|
+
};
|
|
344
|
+
this.hs = void 0;
|
|
345
|
+
} else {
|
|
346
|
+
this.pool = hs.cands;
|
|
347
|
+
this.hs = void 0;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** The candidates a received frame is tried against, in order. */
|
|
351
|
+
candidates() {
|
|
352
|
+
return [...this.hs?.cands ?? [], ...this.pool];
|
|
353
|
+
}
|
|
354
|
+
begin(hs) {
|
|
355
|
+
this.hs = hs;
|
|
356
|
+
}
|
|
357
|
+
/** Ends `hs` if it is still this peer's handshake. */
|
|
358
|
+
end(hs) {
|
|
359
|
+
if (this.hs !== hs) return false;
|
|
360
|
+
this.hs = void 0;
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
confirmCurrent() {
|
|
364
|
+
if (this.current !== void 0) this.current.confirmed = true;
|
|
365
|
+
}
|
|
366
|
+
/** A frame opened under `cand`: it becomes the current channel, the channel it replaces may open in-flight frames for a window, and every other candidate goes. */
|
|
367
|
+
confirm(cand, now) {
|
|
368
|
+
if (this.current !== void 0) this.replaced = {
|
|
369
|
+
channel: this.current.channel,
|
|
370
|
+
remoteKey: this.current.remoteKey,
|
|
371
|
+
until: now + HANDSHAKE_WINDOW_MS
|
|
372
|
+
};
|
|
373
|
+
this.current = {
|
|
374
|
+
...cand,
|
|
375
|
+
confirmed: true
|
|
376
|
+
};
|
|
377
|
+
this.hs = void 0;
|
|
378
|
+
this.pool = [];
|
|
379
|
+
}
|
|
380
|
+
/** Forgets the confirmed channel and the one it replaced, keeping every candidate. */
|
|
381
|
+
dropConfirmed() {
|
|
382
|
+
this.current = void 0;
|
|
383
|
+
this.replaced = void 0;
|
|
384
|
+
}
|
|
385
|
+
/** Forgets the current channel when it is the one that is spent. */
|
|
386
|
+
clearCurrent(channel) {
|
|
387
|
+
if (this.current?.channel === channel) this.current = void 0;
|
|
388
|
+
}
|
|
389
|
+
isEmpty() {
|
|
390
|
+
return this.current === void 0 && this.replaced === void 0 && this.hs === void 0 && this.pool.length === 0;
|
|
391
|
+
}
|
|
392
|
+
/** Resolves once some candidate exists. */
|
|
393
|
+
async waitForChannel() {
|
|
394
|
+
this.waiters ??= (() => {
|
|
395
|
+
const waiting = Promise.withResolvers();
|
|
396
|
+
waiting.promise.catch(() => void 0);
|
|
397
|
+
return waiting;
|
|
398
|
+
})();
|
|
399
|
+
return this.waiters.promise;
|
|
400
|
+
}
|
|
401
|
+
releaseWaiters() {
|
|
402
|
+
this.waiters?.resolve(void 0);
|
|
403
|
+
this.waiters = void 0;
|
|
404
|
+
}
|
|
405
|
+
rejectWaiters(error) {
|
|
406
|
+
this.waiters?.reject(error);
|
|
407
|
+
this.waiters = void 0;
|
|
408
|
+
}
|
|
409
|
+
/** Runs `task` after every earlier one for this peer: two frames opening at once must not both be tried before either has settled what it confirms. */
|
|
410
|
+
async serialise(task) {
|
|
411
|
+
const run = this.opening.then(task);
|
|
412
|
+
this.opening = run.catch(() => void 0);
|
|
413
|
+
return run;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
function authentic(outcome) {
|
|
417
|
+
return outcome.ok || outcome.reason === "undecodable";
|
|
418
|
+
}
|
|
419
|
+
async function openFirst(cands, data) {
|
|
420
|
+
const [first, ...rest] = cands;
|
|
421
|
+
if (first === void 0) return;
|
|
422
|
+
const outcome = await first.channel.openOutcome(data);
|
|
423
|
+
return authentic(outcome) ? {
|
|
424
|
+
cand: first,
|
|
425
|
+
outcome
|
|
426
|
+
} : openFirst(rest, data);
|
|
427
|
+
}
|
|
428
|
+
/** A clock that never goes backwards: milliseconds from an arbitrary origin, as `performance.now()` gives them. What `createMeshSession` gives its channels. */
|
|
429
|
+
const monotonicClock = { now: () => performance.now() };
|
|
280
430
|
function createRelayChannels(identity, sendHello, options = {}) {
|
|
281
431
|
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
432
|
+
const newHandshake = options.newHandshake ?? (async (peer) => beginHandshake(identity, peer));
|
|
433
|
+
let latest = Number.NEGATIVE_INFINITY;
|
|
434
|
+
const source = options.clock ?? monotonicClock;
|
|
435
|
+
const clock = { now: () => {
|
|
436
|
+
latest = Math.max(latest, source.now());
|
|
437
|
+
return latest;
|
|
438
|
+
} };
|
|
439
|
+
const peers = /* @__PURE__ */ new Map();
|
|
440
|
+
const reporter = createFaultReporter(() => clock.now());
|
|
441
|
+
/** Tells the owner of a failure that is local to this side, through the shared fault reporter: at most one report per peer and interval, with the number withheld, and the callback's own failure contained there. */
|
|
442
|
+
function reportFault(error, peer) {
|
|
443
|
+
reporter(peer === void 0 ? "" : deviceIdToHex(peer), async (suppressed) => options.onFault?.(error, {
|
|
444
|
+
peer,
|
|
445
|
+
suppressed
|
|
446
|
+
}));
|
|
286
447
|
}
|
|
287
|
-
|
|
288
|
-
|
|
448
|
+
function prune(entry) {
|
|
449
|
+
if (entry.isEmpty() && !entry.chosen && peers.get(entry.key) === entry) peers.delete(entry.key);
|
|
450
|
+
}
|
|
451
|
+
/** Ends this side's handshake `hs` with the peer, leaving every channel as it is, and tells whoever waits for a channel when there is none to wait for. */
|
|
452
|
+
function dropHandshake(entry, hs, error) {
|
|
453
|
+
if (!entry.end(hs)) return;
|
|
289
454
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
290
|
-
|
|
291
|
-
entry.
|
|
292
|
-
entry
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
455
|
+
hs.sent.reject(failure);
|
|
456
|
+
if (entry.current === void 0 && entry.pool.length === 0) entry.rejectWaiters(failure);
|
|
457
|
+
prune(entry);
|
|
458
|
+
}
|
|
459
|
+
/** Drops every handshake that is waiting for its first answer beyond MAX_PENDING_HANDSHAKES, oldest first. */
|
|
460
|
+
function enforcePendingBound(keep) {
|
|
461
|
+
const pending = [...peers.values()].filter((entry) => entry.hs?.origin === "initiated" && entry.hs.cands.length === 0 && entry.current === void 0);
|
|
462
|
+
const excess = pending.filter((entry) => entry !== keep).sort((a, b) => (a.hs?.sentAt ?? 0) - (b.hs?.sentAt ?? 0)).slice(0, Math.max(0, pending.length - 256));
|
|
463
|
+
for (const entry of excess) if (entry.hs !== void 0) dropHandshake(entry, entry.hs, /* @__PURE__ */ new Error("the handshake was dropped to make room for others"));
|
|
464
|
+
}
|
|
465
|
+
/** Begins this side's own hello to the entry's peer and registers it at once, so a hello arriving meanwhile completes it instead of starting another. */
|
|
466
|
+
function beginOwn(entry, origin) {
|
|
467
|
+
const mine = newHandshake(entry.peer);
|
|
468
|
+
const hs = {
|
|
469
|
+
mine,
|
|
470
|
+
origin,
|
|
471
|
+
sentAt: clock.now(),
|
|
472
|
+
cands: [],
|
|
473
|
+
sent: Promise.withResolvers(),
|
|
474
|
+
expired: false
|
|
307
475
|
};
|
|
308
|
-
|
|
309
|
-
entry.
|
|
310
|
-
|
|
476
|
+
hs.sent.promise.catch(() => void 0);
|
|
477
|
+
entry.begin(hs);
|
|
478
|
+
mine.then(async ({ hello }) => {
|
|
311
479
|
try {
|
|
312
|
-
await sendHello(peer, hello);
|
|
480
|
+
await sendHello(entry.peer, hello);
|
|
313
481
|
} catch (error) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const held = channels.get(key);
|
|
317
|
-
if (held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
318
|
-
}, () => void 0);
|
|
319
|
-
if (answering && track) reportFault(error);
|
|
482
|
+
dropHandshake(entry, hs, error);
|
|
483
|
+
if (origin === "answered") reportFault(error, entry.peer);
|
|
320
484
|
return;
|
|
321
485
|
}
|
|
322
|
-
|
|
486
|
+
hs.sent.resolve(void 0);
|
|
323
487
|
}, (error) => {
|
|
324
|
-
|
|
488
|
+
dropHandshake(entry, hs, error);
|
|
325
489
|
});
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const established = await establishChannelFrom(identity, {
|
|
333
|
-
pending: await entry.pending,
|
|
490
|
+
return hs;
|
|
491
|
+
}
|
|
492
|
+
/** Builds the channel `hello` makes with this side's handshake `hs`, and holds it as a candidate. */
|
|
493
|
+
async function addCandidate(entry, hs, hello, peer) {
|
|
494
|
+
const channel = await establishChannelFrom(identity, {
|
|
495
|
+
pending: await hs.mine,
|
|
334
496
|
remote: hello,
|
|
335
497
|
peer,
|
|
336
498
|
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
337
499
|
});
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (replaces !== void 0) await entry.sent.promise;
|
|
343
|
-
if (channels.get(key) !== (replaces ?? entry)) return;
|
|
344
|
-
const channel = {
|
|
345
|
-
...established,
|
|
346
|
-
seal: async (frame) => {
|
|
347
|
-
try {
|
|
348
|
-
return await established.seal(frame);
|
|
349
|
-
} catch (error) {
|
|
350
|
-
const held = channels.get(key);
|
|
351
|
-
if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
352
|
-
throw error;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
channels.set(key, {
|
|
357
|
-
status: "established",
|
|
500
|
+
if (channel === void 0 || entry.hs !== hs || peers.get(entry.key) !== entry || hs.cands.length >= 2 || entry.holdsKey(hello["ephemeral-key"])) return;
|
|
501
|
+
hs.cands.push({
|
|
358
502
|
channel,
|
|
359
|
-
|
|
503
|
+
remoteKey: hello["ephemeral-key"]
|
|
360
504
|
});
|
|
361
|
-
entry.
|
|
505
|
+
entry.releaseWaiters();
|
|
506
|
+
}
|
|
507
|
+
/** The entry of a peer this side never sent anything to that may be reused now, or undefined when every slot is free or none is old enough, which is told by `slotIsFree`. An entry is reused only once it is older than the window, so answers cannot outpace the turnover of slots. */
|
|
508
|
+
function reusableEntry(now) {
|
|
509
|
+
const [victim] = [...peers.values()].filter((entry) => !entry.chosen && now - entry.lastUsed >= 3e3).sort((a, b) => a.lastUsed - b.lastUsed);
|
|
510
|
+
return victim;
|
|
511
|
+
}
|
|
512
|
+
function slotIsFree() {
|
|
513
|
+
return [...peers.values()].filter((entry) => !entry.chosen).length < 64;
|
|
514
|
+
}
|
|
515
|
+
/** Whether a peer this side never sent anything to could be given an entry now, changing nothing. */
|
|
516
|
+
function slotAvailable(now) {
|
|
517
|
+
return slotIsFree() || reusableEntry(now) !== void 0;
|
|
518
|
+
}
|
|
519
|
+
/** Makes room for a peer this side never sent anything to. */
|
|
520
|
+
function reserveUnsolicitedSlot(now) {
|
|
521
|
+
if (slotIsFree()) return true;
|
|
522
|
+
const victim = reusableEntry(now);
|
|
523
|
+
if (victim === void 0) return false;
|
|
524
|
+
discard(victim, /* @__PURE__ */ new Error("the entry was dropped to make room for others"));
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
function discard(entry, error) {
|
|
528
|
+
peers.delete(entry.key);
|
|
529
|
+
entry.hs?.sent.reject(error);
|
|
530
|
+
entry.rejectWaiters(error);
|
|
531
|
+
}
|
|
532
|
+
async function sealUnder(entry, channel, frame) {
|
|
533
|
+
try {
|
|
534
|
+
return await channel.seal(frame);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
if (error instanceof ChannelExhaustedError) entry.clearCurrent(channel);
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function sealFor(entry, frame) {
|
|
541
|
+
entry.expire(clock.now());
|
|
542
|
+
entry.touch(clock.now());
|
|
543
|
+
const { current, hs } = entry;
|
|
544
|
+
if (current !== void 0) return [await sealUnder(entry, current.channel, frame)];
|
|
545
|
+
if (entry.pool.length > 0) return Promise.all(entry.pool.map(async (cand) => sealUnder(entry, cand.channel, frame)));
|
|
546
|
+
if (hs === void 0) {
|
|
547
|
+
beginOwn(entry, "initiated");
|
|
548
|
+
enforcePendingBound(entry);
|
|
549
|
+
return sealFor(entry, frame);
|
|
550
|
+
}
|
|
551
|
+
if (hs.cands.length > 0) {
|
|
552
|
+
await hs.sent.promise;
|
|
553
|
+
return Promise.all(hs.cands.map(async (cand) => sealUnder(entry, cand.channel, frame)));
|
|
554
|
+
}
|
|
555
|
+
await entry.waitForChannel();
|
|
556
|
+
return sealFor(entry, frame);
|
|
557
|
+
}
|
|
558
|
+
/** Tries a frame against the peer's channels, in order: the current one, then the candidates, then the replaced one while its window lasts. */
|
|
559
|
+
async function openUnder(entry, data) {
|
|
560
|
+
entry.expire(clock.now());
|
|
561
|
+
const { current } = entry;
|
|
562
|
+
if (current !== void 0) {
|
|
563
|
+
const outcome = await current.channel.openOutcome(data);
|
|
564
|
+
if (authentic(outcome)) {
|
|
565
|
+
entry.confirmCurrent();
|
|
566
|
+
entry.touch(clock.now());
|
|
567
|
+
return outcome.ok ? {
|
|
568
|
+
frame: outcome.frame,
|
|
569
|
+
from: current.channel.peer.deviceId
|
|
570
|
+
} : void 0;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const found = await openFirst(entry.candidates(), data);
|
|
574
|
+
if (found !== void 0) {
|
|
575
|
+
if (peers.get(entry.key) !== entry) return;
|
|
576
|
+
entry.confirm(found.cand, clock.now());
|
|
577
|
+
entry.touch(clock.now());
|
|
578
|
+
return found.outcome.ok ? {
|
|
579
|
+
frame: found.outcome.frame,
|
|
580
|
+
from: found.cand.channel.peer.deviceId
|
|
581
|
+
} : void 0;
|
|
582
|
+
}
|
|
583
|
+
const { replaced } = entry;
|
|
584
|
+
if (replaced !== void 0 && clock.now() < replaced.until) {
|
|
585
|
+
const outcome = await replaced.channel.openOutcome(data);
|
|
586
|
+
return outcome.ok ? {
|
|
587
|
+
frame: outcome.frame,
|
|
588
|
+
from: replaced.channel.peer.deviceId
|
|
589
|
+
} : void 0;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function acceptHello(hello, sender) {
|
|
593
|
+
const key = deviceIdToHex(sender);
|
|
594
|
+
const verdict = await verifyHello(identity, hello);
|
|
595
|
+
if (!verdict.ok || !await isUsableEphemeralKey(hello["ephemeral-key"])) return;
|
|
596
|
+
const now = clock.now();
|
|
597
|
+
let entry = peers.get(key);
|
|
598
|
+
entry?.expire(now);
|
|
599
|
+
if (entry?.holdsKey(hello["ephemeral-key"]) === true) return;
|
|
600
|
+
if (entry?.hs !== void 0 && entry.outstanding(now)) {
|
|
601
|
+
if (entry.hs.cands.length < 2) await addCandidate(entry, entry.hs, hello, verdict.peer);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (entry === void 0) {
|
|
605
|
+
if (!reserveUnsolicitedSlot(now)) return;
|
|
606
|
+
entry = new PeerChannels(sender, now);
|
|
607
|
+
peers.set(key, entry);
|
|
608
|
+
}
|
|
609
|
+
const hs = beginOwn(entry, "answered");
|
|
610
|
+
entry.touch(now);
|
|
611
|
+
try {
|
|
612
|
+
await addCandidate(entry, hs, hello, verdict.peer);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
dropHandshake(entry, hs, error);
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
362
617
|
}
|
|
363
618
|
return {
|
|
364
|
-
|
|
619
|
+
seal: async (peer, frame, purpose) => {
|
|
365
620
|
const key = deviceIdToHex(peer);
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
621
|
+
const now = clock.now();
|
|
622
|
+
let entry = peers.get(key);
|
|
623
|
+
if (entry === void 0) {
|
|
624
|
+
if (purpose === "reply" && !reserveUnsolicitedSlot(now)) throw new Error("there is no channel with this peer");
|
|
625
|
+
entry = new PeerChannels(peer, now);
|
|
626
|
+
peers.set(key, entry);
|
|
627
|
+
}
|
|
628
|
+
if (purpose === "request") entry.choose();
|
|
629
|
+
return sealFor(entry, frame);
|
|
369
630
|
},
|
|
370
|
-
applyHello: async (hello) => {
|
|
371
|
-
let
|
|
631
|
+
applyHello: async (hello, claimedFrom) => {
|
|
632
|
+
let sender;
|
|
372
633
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
await complete({
|
|
384
|
-
entry,
|
|
385
|
-
hello,
|
|
386
|
-
peer: verdict.peer,
|
|
387
|
-
replaces
|
|
388
|
-
});
|
|
634
|
+
if (claimedFrom === void 0) return;
|
|
635
|
+
sender = await identity.deriveDeviceId(hello["identity-key"]["public-key"]);
|
|
636
|
+
if (!bytesEqual(sender, claimedFrom)) return;
|
|
637
|
+
const held = peers.get(deviceIdToHex(sender));
|
|
638
|
+
const now = clock.now();
|
|
639
|
+
held?.expire(now);
|
|
640
|
+
if (held?.holdsKey(hello["ephemeral-key"]) === true) return;
|
|
641
|
+
if (held?.hs !== void 0 && held.outstanding(now) && held.hs.cands.length >= 2) return;
|
|
642
|
+
if (held === void 0 && !slotAvailable(now)) return;
|
|
643
|
+
await acceptHello(hello, sender);
|
|
389
644
|
} catch (error) {
|
|
390
|
-
|
|
391
|
-
reportFault(error);
|
|
645
|
+
reportFault(error, sender);
|
|
392
646
|
}
|
|
393
647
|
},
|
|
394
648
|
open: async (data, claimedFrom) => {
|
|
395
649
|
if (claimedFrom === void 0) return;
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const frame = await entry.channel.open(data);
|
|
400
|
-
return frame === void 0 ? void 0 : {
|
|
401
|
-
frame,
|
|
402
|
-
from: entry.channel.peer.deviceId
|
|
403
|
-
};
|
|
650
|
+
const entry = peers.get(deviceIdToHex(claimedFrom));
|
|
651
|
+
if (entry === void 0) return;
|
|
652
|
+
return entry.serialise(async () => openUnder(entry, data));
|
|
404
653
|
},
|
|
405
654
|
reset: (reason) => {
|
|
406
|
-
for (const entry of [...
|
|
407
|
-
channels.clear();
|
|
655
|
+
for (const entry of [...peers.values()]) discard(entry, new Error(reason));
|
|
408
656
|
},
|
|
409
657
|
forget: (peer) => {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
if (entry?.
|
|
413
|
-
|
|
658
|
+
const entry = peers.get(deviceIdToHex(peer));
|
|
659
|
+
if (entry === void 0) return;
|
|
660
|
+
if ((entry.hs?.cands.length ?? 0) + entry.pool.length === 0) {
|
|
661
|
+
discard(entry, /* @__PURE__ */ new Error("the relay pairing was lost"));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
entry.dropConfirmed();
|
|
414
665
|
}
|
|
415
666
|
};
|
|
416
667
|
}
|
|
@@ -499,15 +750,15 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
499
750
|
return connection;
|
|
500
751
|
}
|
|
501
752
|
/** 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. */
|
|
502
|
-
async function transmit(frame,
|
|
753
|
+
async function transmit(frame, toDevice, purpose = "request") {
|
|
503
754
|
if (connection === null) throw new Error("not connected");
|
|
504
|
-
if (
|
|
755
|
+
if (toDevice === void 0) {
|
|
505
756
|
await connection.send(frame);
|
|
506
757
|
return;
|
|
507
758
|
}
|
|
508
|
-
|
|
509
|
-
const sealed = await
|
|
510
|
-
await
|
|
759
|
+
const link = connection;
|
|
760
|
+
const sealed = await relayChannels.seal(toDevice, frame, purpose);
|
|
761
|
+
await Promise.all(sealed.map(async (copy) => link.send(wrapRelayData(copy, toDevice))));
|
|
511
762
|
}
|
|
512
763
|
/** `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. */
|
|
513
764
|
function applyManageResponse(frame, from) {
|
|
@@ -541,7 +792,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
541
792
|
direction: "sent",
|
|
542
793
|
frame: response
|
|
543
794
|
});
|
|
544
|
-
await transmit(response,
|
|
795
|
+
await transmit(response, fromDevice, "reply");
|
|
545
796
|
emit();
|
|
546
797
|
}
|
|
547
798
|
};
|
|
@@ -559,7 +810,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
559
810
|
direction: "received",
|
|
560
811
|
frame: inner
|
|
561
812
|
});
|
|
562
|
-
await relayChannels.applyHello(inner);
|
|
813
|
+
await relayChannels.applyHello(inner, frame["from-device"]);
|
|
563
814
|
return;
|
|
564
815
|
}
|
|
565
816
|
if (inner?.type === "secure-data") {
|
|
@@ -846,7 +1097,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
846
1097
|
});
|
|
847
1098
|
const exchange = (async () => {
|
|
848
1099
|
try {
|
|
849
|
-
await transmit(frame, targetDevice
|
|
1100
|
+
await transmit(frame, targetDevice);
|
|
850
1101
|
} catch (error) {
|
|
851
1102
|
pendingManageRequests.delete(requestId);
|
|
852
1103
|
throw error;
|
|
@@ -883,7 +1134,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
883
1134
|
direction: "sent",
|
|
884
1135
|
frame
|
|
885
1136
|
});
|
|
886
|
-
await transmit(frame
|
|
1137
|
+
await transmit(frame);
|
|
887
1138
|
emit();
|
|
888
1139
|
},
|
|
889
1140
|
async sendCoordinatorClaim(frame) {
|
|
@@ -893,7 +1144,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
893
1144
|
direction: "sent",
|
|
894
1145
|
frame: claim
|
|
895
1146
|
});
|
|
896
|
-
await transmit(claim
|
|
1147
|
+
await transmit(claim);
|
|
897
1148
|
emit();
|
|
898
1149
|
},
|
|
899
1150
|
async sendGossipUpdate(extensions) {
|
|
@@ -903,7 +1154,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
903
1154
|
direction: "sent",
|
|
904
1155
|
frame
|
|
905
1156
|
});
|
|
906
|
-
await transmit(frame
|
|
1157
|
+
await transmit(frame);
|
|
907
1158
|
emit();
|
|
908
1159
|
},
|
|
909
1160
|
getTopologyPeers() {
|
|
@@ -916,7 +1167,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
916
1167
|
direction: "sent",
|
|
917
1168
|
frame
|
|
918
1169
|
});
|
|
919
|
-
await transmit(frame, targetDevice
|
|
1170
|
+
await transmit(frame, targetDevice);
|
|
920
1171
|
emit();
|
|
921
1172
|
},
|
|
922
1173
|
async close() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, H as ManageCommandParams, Q as PathTrace, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-Cb3BnuTL.cjs";
|
|
2
2
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
3
|
-
import { IncomingManageRequest, ManageOutcome, MeshSession } from "
|
|
3
|
+
import { d as IncomingManageRequest, f as ManageOutcome, p as MeshSession } from "../mesh-session-By06Mnu-.cjs";
|
|
4
4
|
//#region src/domain/path-trace.d.ts
|
|
5
5
|
/** path:trace names no real capability -- see spec/management.cddl's own comment -- and is registered purely as a namespaced wire identifier so manage-command.verb's own capability-verb grammar has something syntactically valid to carry (registry/core-capabilities.md). */
|
|
6
6
|
export declare const PATH_TRACE_VERB = "path:trace";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, H as ManageCommandParams, Q as PathTrace, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-Cb3BnuTL.mjs";
|
|
2
2
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
3
|
-
import { IncomingManageRequest, ManageOutcome, MeshSession } from "
|
|
3
|
+
import { d as IncomingManageRequest, f as ManageOutcome, p as MeshSession } from "../mesh-session-DzcZYdhe.mjs";
|
|
4
4
|
//#region src/domain/path-trace.d.ts
|
|
5
5
|
/** path:trace names no real capability -- see spec/management.cddl's own comment -- and is registered purely as a namespaced wire identifier so manage-command.verb's own capability-verb grammar has something syntactically valid to carry (registry/core-capabilities.md). */
|
|
6
6
|
export declare const PATH_TRACE_VERB = "path:trace";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-Cb3BnuTL.cjs";
|
|
2
2
|
import { r as IdentityPort } from "../identity-CQof0iEM.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
|
-
import { IncomingManageRequest, ManageOutcome, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, f as ManageOutcome, p as MeshSession } from "../mesh-session-By06Mnu-.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
6
6
|
//#region src/domain/room-rekey.d.ts
|
|
7
7
|
/** The error code a room.rekey is answered with when it unwrapped but the recipient could not store the keys, so the sender knows the key is not held and sends it again. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-Cb3BnuTL.mjs";
|
|
2
2
|
import { r as IdentityPort } from "../identity-CUr-jaAB.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
|
-
import { IncomingManageRequest, ManageOutcome, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, f as ManageOutcome, p as MeshSession } from "../mesh-session-DzcZYdhe.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
|
6
6
|
//#region src/domain/room-rekey.d.ts
|
|
7
7
|
/** The error code a room.rekey is answered with when it unwrapped but the recipient could not store the keys, so the sender knows the key is not held and sends it again. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, et as PeerAdvert, nn as TopologyPeers } from "../protocol-Cb3BnuTL.cjs";
|
|
2
|
-
import { DirectoryEntry, IncomingManageRequest, MeshSession } from "
|
|
2
|
+
import { a as DirectoryEntry, d as IncomingManageRequest, p as MeshSession } from "../mesh-session-By06Mnu-.cjs";
|
|
3
3
|
//#region src/domain/topology.d.ts
|
|
4
4
|
export declare const TOPOLOGY_GET_CAPABILITY = "topology:get";
|
|
5
5
|
/** Builds the topology.get manage-command per management.cddl: the outer verb is the registered `topology:get` capability, matching every other domain-specific (non-generic) manage-command's own "outer verb is a real capability-verb" convention. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, et as PeerAdvert, nn as TopologyPeers } from "../protocol-Cb3BnuTL.mjs";
|
|
2
|
-
import { DirectoryEntry, IncomingManageRequest, MeshSession } from "
|
|
2
|
+
import { a as DirectoryEntry, d as IncomingManageRequest, p as MeshSession } from "../mesh-session-DzcZYdhe.mjs";
|
|
3
3
|
//#region src/domain/topology.d.ts
|
|
4
4
|
export declare const TOPOLOGY_GET_CAPABILITY = "topology:get";
|
|
5
5
|
/** Builds the topology.get manage-command per management.cddl: the outer verb is the registered `topology:get` capability, matching every other domain-specific (non-generic) manage-command's own "outer verb is a real capability-verb" convention. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { H as ManageCommandParams, Lt as SfuTrackEntry, Rt as SfuTrackMap, V as ManageCommand, an as WebrtcAnswer, f as CapabilityScope, on as WebrtcIceCandidate, sn as WebrtcOffer, z as IceCandidateInit } from "../protocol-Cb3BnuTL.cjs";
|
|
2
|
-
import { IncomingManageRequest } from "
|
|
2
|
+
import { d as IncomingManageRequest } from "../mesh-session-By06Mnu-.cjs";
|
|
3
3
|
import { VerifyCapabilityTokenOptions } from "./tokens.cjs";
|
|
4
4
|
//#region src/domain/webrtc-signaling.d.ts
|
|
5
5
|
/** The one capability verb gating every core/webrtc message shape: an authority over this node's own signaling as a whole, not three separate resources, mirroring how core/exec's exec:pty gates all of its own inner verbs. webrtc.sfu-track-map is gated by this same verb (see spec/webrtc.cddl's own comment): an SFU able to shape a negotiation can already misreport whose media is on which mid, so a separate capability over the track-map itself would guard nothing a client doesn't already have to trust the SFU for. */
|