wire-mesh-core 4.1.1 → 4.1.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.
@@ -280,73 +280,129 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
280
280
  function createRelayChannels(identity, sendHello, options = {}) {
281
281
  if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
282
282
  const channels = /* @__PURE__ */ new Map();
283
- /** 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. */
284
- function start(peer) {
283
+ /** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it is reported once and swallowed. */
284
+ function reportFault(error) {
285
+ const report = (faultError) => {
286
+ console.error("the local fault callback failed", faultError);
287
+ };
288
+ try {
289
+ const result = options.onFault?.(error);
290
+ if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
291
+ } catch (faultError) {
292
+ report(faultError);
293
+ }
294
+ }
295
+ /** Abandons a handshake that failed: the entry goes if it is still the one held for the peer, and whoever awaits it is told. */
296
+ function abandon(entry, error) {
297
+ const failure = error instanceof Error ? error : new Error(String(error));
298
+ if (channels.get(entry.key) === entry) channels.delete(entry.key);
299
+ entry.settle.reject(failure);
300
+ entry.sent.reject(failure);
301
+ }
302
+ /** Starts a handshake with the peer: sends this side's hello, and registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second. `answering` is true when a received hello started this, so nobody awaits the outcome and a failure to send is reported to the owner. `track` is false for a handshake answering a hello while a channel with that peer is established: it is not registered, so the established channel stays in place and in use until the new one is up and this side's hello has been sent. A hello that could not be sent leaves no channel behind: the peer would never learn this side's key. */
303
+ function start(peer, { answering, track } = {
304
+ answering: false,
305
+ track: true
306
+ }) {
285
307
  const key = require_domain_device_id.deviceIdToHex(peer);
286
- const settle = Promise.withResolvers();
287
308
  const pending = beginHandshake(identity, peer);
288
309
  const entry = {
289
310
  status: "awaiting",
290
311
  pending,
291
- settle
312
+ settle: Promise.withResolvers(),
313
+ key,
314
+ sent: Promise.withResolvers()
292
315
  };
293
- channels.set(key, entry);
294
- pending.then(async ({ hello }) => sendHello(peer, hello)).catch((error) => {
295
- settle.reject(error instanceof Error ? error : new Error(String(error)));
296
- if (channels.get(key) === entry) channels.delete(key);
316
+ entry.settle.promise.catch(() => void 0);
317
+ entry.sent.promise.catch(() => void 0);
318
+ pending.then(async ({ hello }) => {
319
+ try {
320
+ await sendHello(peer, hello);
321
+ } catch (error) {
322
+ abandon(entry, error);
323
+ entry.settle.promise.then((channel) => {
324
+ const held = channels.get(key);
325
+ if (held?.status === "established" && held.channel === channel) channels.delete(key);
326
+ }, () => void 0);
327
+ if (answering && track) reportFault(error);
328
+ return;
329
+ }
330
+ entry.sent.resolve(void 0);
331
+ }, (error) => {
332
+ abandon(entry, error);
297
333
  });
334
+ if (track) channels.set(key, entry);
298
335
  return entry;
299
336
  }
337
+ /** Completes the handshake a verified hello belongs to. Throws on any failure of this side; the caller abandons the handshake. */
338
+ async function complete({ entry, hello, peer, replaces }) {
339
+ const { key } = entry;
340
+ const established = await establishChannelFrom(identity, {
341
+ pending: await entry.pending,
342
+ remote: hello,
343
+ peer,
344
+ ...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
345
+ });
346
+ if (established === void 0) {
347
+ abandon(entry, /* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
348
+ return;
349
+ }
350
+ if (replaces !== void 0) await entry.sent.promise;
351
+ if (channels.get(key) !== (replaces ?? entry)) return;
352
+ const channel = {
353
+ ...established,
354
+ seal: async (frame) => {
355
+ try {
356
+ return await established.seal(frame);
357
+ } catch (error) {
358
+ const held = channels.get(key);
359
+ if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
360
+ throw error;
361
+ }
362
+ }
363
+ };
364
+ channels.set(key, {
365
+ status: "established",
366
+ channel,
367
+ remoteEphemeralKey: hello["ephemeral-key"]
368
+ });
369
+ entry.settle.resolve(channel);
370
+ }
300
371
  return {
301
372
  ensure: async (peer) => {
302
- const existing = channels.get(require_domain_device_id.deviceIdToHex(peer));
373
+ const key = require_domain_device_id.deviceIdToHex(peer);
374
+ const existing = channels.get(key);
303
375
  if (existing?.status === "established") return existing.channel;
304
376
  return (existing ?? start(peer)).settle.promise;
305
377
  },
306
378
  applyHello: async (hello) => {
307
- let verdict;
379
+ let entry;
308
380
  try {
309
- verdict = await verifyHello(identity, hello);
381
+ const verdict = await verifyHello(identity, hello);
382
+ if (!verdict.ok) return;
383
+ const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
384
+ const current = channels.get(key);
385
+ if (current?.status === "established" && require_domain_token_scope.bytesEqual(current.remoteEphemeralKey, hello["ephemeral-key"])) return;
386
+ const replaces = current?.status === "established" ? current : void 0;
387
+ entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId, {
388
+ answering: true,
389
+ track: replaces === void 0
390
+ });
391
+ await complete({
392
+ entry,
393
+ hello,
394
+ peer: verdict.peer,
395
+ replaces
396
+ });
310
397
  } catch (error) {
311
- options.onFault?.(error);
312
- return;
313
- }
314
- if (!verdict.ok) return;
315
- const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
316
- const current = channels.get(key);
317
- const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
318
- const established = await establishChannelFrom(identity, {
319
- pending: await entry.pending,
320
- remote: hello,
321
- peer: verdict.peer,
322
- ...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
323
- });
324
- if (established === void 0) {
325
- entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
326
- if (channels.get(key) === entry) channels.delete(key);
327
- return;
398
+ if (entry !== void 0) abandon(entry, error);
399
+ reportFault(error);
328
400
  }
329
- const channel = {
330
- ...established,
331
- seal: async (frame) => {
332
- try {
333
- return await established.seal(frame);
334
- } catch (error) {
335
- const held = channels.get(key);
336
- if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
337
- throw error;
338
- }
339
- }
340
- };
341
- channels.set(key, {
342
- status: "established",
343
- channel
344
- });
345
- entry.settle.resolve(channel);
346
401
  },
347
402
  open: async (data, claimedFrom) => {
348
403
  if (claimedFrom === void 0) return;
349
- const entry = channels.get(require_domain_device_id.deviceIdToHex(claimedFrom));
404
+ const key = require_domain_device_id.deviceIdToHex(claimedFrom);
405
+ const entry = channels.get(key);
350
406
  if (entry?.status !== "established") return;
351
407
  const frame = await entry.channel.open(data);
352
408
  return frame === void 0 ? void 0 : {
@@ -355,13 +411,13 @@ function createRelayChannels(identity, sendHello, options = {}) {
355
411
  };
356
412
  },
357
413
  reset: (reason) => {
358
- for (const entry of channels.values()) if (entry.status === "awaiting") entry.settle.reject(new Error(reason));
414
+ for (const entry of [...channels.values()]) if (entry.status === "awaiting") abandon(entry, new Error(reason));
359
415
  channels.clear();
360
416
  },
361
417
  forget: (peer) => {
362
418
  const key = require_domain_device_id.deviceIdToHex(peer);
363
419
  const entry = channels.get(key);
364
- if (entry?.status === "awaiting") entry.settle.reject(/* @__PURE__ */ new Error("the relay pairing was lost"));
420
+ if (entry?.status === "awaiting") abandon(entry, /* @__PURE__ */ new Error("the relay pairing was lost"));
365
421
  channels.delete(key);
366
422
  }
367
423
  };
@@ -137,8 +137,8 @@ addresses?: readonly string[],
137
137
  onPeerAdvert?: (advert: PeerAdvert) => void,
138
138
  /** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
139
139
  onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
140
- /** Told of a failure that is local to this session and did not end it: an identity port that failed while checking a hello from a peer. The hello is dropped and the peer is told nothing. Omit for a caller with no use for it: the failure is then not reported anywhere. */
141
- onLocalFault?: (error: unknown) => void): MeshSession;
140
+ /** Told of a failure that is local to this session and did not end it, while handling a hello from a peer: an identity port that failed to check or sign, an answering hello that could not be sent, a channel that could not be established. The hello is dropped, any half-made handshake for that peer is discarded, and the peer is told nothing, so no failure in handling a hello ends the session. Called once per failing hello; if it throws, or returns a promise that rejects, that is reported once to the console and swallowed. Omit for a caller with no use for it. */
141
+ onLocalFault?: (error: unknown) => void | Promise<void>): MeshSession;
142
142
  /** 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. */
143
143
  export interface AcceptedMeshSession extends MeshSession {
144
144
  /** 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. */
@@ -137,8 +137,8 @@ addresses?: readonly string[],
137
137
  onPeerAdvert?: (advert: PeerAdvert) => void,
138
138
  /** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
139
139
  onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
140
- /** Told of a failure that is local to this session and did not end it: an identity port that failed while checking a hello from a peer. The hello is dropped and the peer is told nothing. Omit for a caller with no use for it: the failure is then not reported anywhere. */
141
- onLocalFault?: (error: unknown) => void): MeshSession;
140
+ /** Told of a failure that is local to this session and did not end it, while handling a hello from a peer: an identity port that failed to check or sign, an answering hello that could not be sent, a channel that could not be established. The hello is dropped, any half-made handshake for that peer is discarded, and the peer is told nothing, so no failure in handling a hello ends the session. Called once per failing hello; if it throws, or returns a promise that rejects, that is reported once to the console and swallowed. Omit for a caller with no use for it. */
141
+ onLocalFault?: (error: unknown) => void | Promise<void>): MeshSession;
142
142
  /** 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. */
143
143
  export interface AcceptedMeshSession extends MeshSession {
144
144
  /** 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. */
@@ -279,73 +279,129 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
279
279
  function createRelayChannels(identity, sendHello, options = {}) {
280
280
  if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
281
281
  const channels = /* @__PURE__ */ new Map();
282
- /** 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. */
283
- function start(peer) {
282
+ /** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it is reported once and swallowed. */
283
+ function reportFault(error) {
284
+ const report = (faultError) => {
285
+ console.error("the local fault callback failed", faultError);
286
+ };
287
+ try {
288
+ const result = options.onFault?.(error);
289
+ if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
290
+ } catch (faultError) {
291
+ report(faultError);
292
+ }
293
+ }
294
+ /** Abandons a handshake that failed: the entry goes if it is still the one held for the peer, and whoever awaits it is told. */
295
+ function abandon(entry, error) {
296
+ const failure = error instanceof Error ? error : new Error(String(error));
297
+ if (channels.get(entry.key) === entry) channels.delete(entry.key);
298
+ entry.settle.reject(failure);
299
+ entry.sent.reject(failure);
300
+ }
301
+ /** Starts a handshake with the peer: sends this side's hello, and registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second. `answering` is true when a received hello started this, so nobody awaits the outcome and a failure to send is reported to the owner. `track` is false for a handshake answering a hello while a channel with that peer is established: it is not registered, so the established channel stays in place and in use until the new one is up and this side's hello has been sent. A hello that could not be sent leaves no channel behind: the peer would never learn this side's key. */
302
+ function start(peer, { answering, track } = {
303
+ answering: false,
304
+ track: true
305
+ }) {
284
306
  const key = deviceIdToHex(peer);
285
- const settle = Promise.withResolvers();
286
307
  const pending = beginHandshake(identity, peer);
287
308
  const entry = {
288
309
  status: "awaiting",
289
310
  pending,
290
- settle
311
+ settle: Promise.withResolvers(),
312
+ key,
313
+ sent: Promise.withResolvers()
291
314
  };
292
- channels.set(key, entry);
293
- pending.then(async ({ hello }) => sendHello(peer, hello)).catch((error) => {
294
- settle.reject(error instanceof Error ? error : new Error(String(error)));
295
- if (channels.get(key) === entry) channels.delete(key);
315
+ entry.settle.promise.catch(() => void 0);
316
+ entry.sent.promise.catch(() => void 0);
317
+ pending.then(async ({ hello }) => {
318
+ try {
319
+ await sendHello(peer, hello);
320
+ } catch (error) {
321
+ abandon(entry, error);
322
+ entry.settle.promise.then((channel) => {
323
+ const held = channels.get(key);
324
+ if (held?.status === "established" && held.channel === channel) channels.delete(key);
325
+ }, () => void 0);
326
+ if (answering && track) reportFault(error);
327
+ return;
328
+ }
329
+ entry.sent.resolve(void 0);
330
+ }, (error) => {
331
+ abandon(entry, error);
296
332
  });
333
+ if (track) channels.set(key, entry);
297
334
  return entry;
298
335
  }
336
+ /** Completes the handshake a verified hello belongs to. Throws on any failure of this side; the caller abandons the handshake. */
337
+ async function complete({ entry, hello, peer, replaces }) {
338
+ const { key } = entry;
339
+ const established = await establishChannelFrom(identity, {
340
+ pending: await entry.pending,
341
+ remote: hello,
342
+ peer,
343
+ ...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
344
+ });
345
+ if (established === void 0) {
346
+ abandon(entry, /* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
347
+ return;
348
+ }
349
+ if (replaces !== void 0) await entry.sent.promise;
350
+ if (channels.get(key) !== (replaces ?? entry)) return;
351
+ const channel = {
352
+ ...established,
353
+ seal: async (frame) => {
354
+ try {
355
+ return await established.seal(frame);
356
+ } catch (error) {
357
+ const held = channels.get(key);
358
+ if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
359
+ throw error;
360
+ }
361
+ }
362
+ };
363
+ channels.set(key, {
364
+ status: "established",
365
+ channel,
366
+ remoteEphemeralKey: hello["ephemeral-key"]
367
+ });
368
+ entry.settle.resolve(channel);
369
+ }
299
370
  return {
300
371
  ensure: async (peer) => {
301
- const existing = channels.get(deviceIdToHex(peer));
372
+ const key = deviceIdToHex(peer);
373
+ const existing = channels.get(key);
302
374
  if (existing?.status === "established") return existing.channel;
303
375
  return (existing ?? start(peer)).settle.promise;
304
376
  },
305
377
  applyHello: async (hello) => {
306
- let verdict;
378
+ let entry;
307
379
  try {
308
- verdict = await verifyHello(identity, hello);
380
+ const verdict = await verifyHello(identity, hello);
381
+ if (!verdict.ok) return;
382
+ const key = deviceIdToHex(verdict.peer.deviceId);
383
+ const current = channels.get(key);
384
+ if (current?.status === "established" && bytesEqual(current.remoteEphemeralKey, hello["ephemeral-key"])) return;
385
+ const replaces = current?.status === "established" ? current : void 0;
386
+ entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId, {
387
+ answering: true,
388
+ track: replaces === void 0
389
+ });
390
+ await complete({
391
+ entry,
392
+ hello,
393
+ peer: verdict.peer,
394
+ replaces
395
+ });
309
396
  } catch (error) {
310
- options.onFault?.(error);
311
- return;
312
- }
313
- if (!verdict.ok) return;
314
- const key = deviceIdToHex(verdict.peer.deviceId);
315
- const current = channels.get(key);
316
- const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
317
- const established = await establishChannelFrom(identity, {
318
- pending: await entry.pending,
319
- remote: hello,
320
- peer: verdict.peer,
321
- ...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
322
- });
323
- if (established === void 0) {
324
- entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
325
- if (channels.get(key) === entry) channels.delete(key);
326
- return;
397
+ if (entry !== void 0) abandon(entry, error);
398
+ reportFault(error);
327
399
  }
328
- const channel = {
329
- ...established,
330
- seal: async (frame) => {
331
- try {
332
- return await established.seal(frame);
333
- } catch (error) {
334
- const held = channels.get(key);
335
- if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
336
- throw error;
337
- }
338
- }
339
- };
340
- channels.set(key, {
341
- status: "established",
342
- channel
343
- });
344
- entry.settle.resolve(channel);
345
400
  },
346
401
  open: async (data, claimedFrom) => {
347
402
  if (claimedFrom === void 0) return;
348
- const entry = channels.get(deviceIdToHex(claimedFrom));
403
+ const key = deviceIdToHex(claimedFrom);
404
+ const entry = channels.get(key);
349
405
  if (entry?.status !== "established") return;
350
406
  const frame = await entry.channel.open(data);
351
407
  return frame === void 0 ? void 0 : {
@@ -354,13 +410,13 @@ function createRelayChannels(identity, sendHello, options = {}) {
354
410
  };
355
411
  },
356
412
  reset: (reason) => {
357
- for (const entry of channels.values()) if (entry.status === "awaiting") entry.settle.reject(new Error(reason));
413
+ for (const entry of [...channels.values()]) if (entry.status === "awaiting") abandon(entry, new Error(reason));
358
414
  channels.clear();
359
415
  },
360
416
  forget: (peer) => {
361
417
  const key = deviceIdToHex(peer);
362
418
  const entry = channels.get(key);
363
- if (entry?.status === "awaiting") entry.settle.reject(/* @__PURE__ */ new Error("the relay pairing was lost"));
419
+ if (entry?.status === "awaiting") abandon(entry, /* @__PURE__ */ new Error("the relay pairing was lost"));
364
420
  channels.delete(key);
365
421
  }
366
422
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "1.0.1",