wire-mesh-core 3.5.1 → 3.6.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.
@@ -81,7 +81,17 @@ export interface MeshSession {
81
81
  readonly coordinatorFrames: AsyncIterable<CoordinatorFrame>;
82
82
  connect: (address: string, localDomains: readonly string[]) => Promise<void>;
83
83
  sendPing: () => Promise<void>;
84
- /** Sends a ping-frame and resolves with the round-trip time in milliseconds once the correlated pong-frame arrives -- FIFO-paired against this call's own ping, since ping-frame carries no correlation id of its own (spec/transport.cddl): the Nth call's own promise resolves against the Nth pong received after it, never matched by any other means. Rejects if the connection closes, or (when timeoutMs is given) if no pong arrives within timeoutMs, rather than resolving a sentinel value the way sendManageRequest's own timeout does -- there is no natural "no answer" value for a bare millisecond count to double as. Unlike sendPing (fire-and-forget, answered by nothing on an ordinary peer connection), this is answered only by a peer that replies to ping with pong -- today, relay-hub's own frame handling (wire-mesh#181) -- so calling this against a connection to a plain peer that never sends pong hangs until timeoutMs (if given) or forever. Exists to isolate the sender-to-hub leg of a relayed path.trace round trip: time this over the same connection a relayed manage-request travelled, then subtract it from path.trace's own end-to-end RTT to recover the hub-to-target leg. */
84
+ /**
85
+ * Sends a ping-frame and resolves with the round-trip time in milliseconds once the correlated pong-frame arrives.
86
+ *
87
+ * The pairing is FIFO because ping-frame carries no correlation id of its own (spec/transport.cddl): the Nth call's promise resolves against the Nth pong received after it, never matched by any other means.
88
+ *
89
+ * Rejects with a plain Error if the connection closes, and with a PingTimeoutError (from ping-round-trips.ts) when `timeoutMs` is given and no pong arrives within it, so a caller can tell an unanswered ping from a dropped connection. It never resolves a sentinel the way sendManageRequest's own timeout does, because there is no natural "no answer" value for a bare millisecond count to double as.
90
+ *
91
+ * Unlike sendPing (fire-and-forget, answered by nothing on an ordinary peer connection), this is answered only by a peer that replies to ping with pong, which today means relay-hub's own frame handling (wire-mesh#181). Called against a plain peer that never sends pong it hangs until `timeoutMs` if given, or forever.
92
+ *
93
+ * It exists to isolate the sender-to-hub leg of a relayed path.trace round trip: time it over the same connection a relayed manage-request travelled, then subtract it from path.trace's own end-to-end RTT to recover the hub-to-target leg.
94
+ */
85
95
  sendPingMeasureRtt: (timeoutMs?: number) => Promise<number>;
86
96
  /** Attaches this token to every `manage-request` sent from now on. */
87
97
  setToken: (token: CapabilityToken) => void;
@@ -81,7 +81,17 @@ export interface MeshSession {
81
81
  readonly coordinatorFrames: AsyncIterable<CoordinatorFrame>;
82
82
  connect: (address: string, localDomains: readonly string[]) => Promise<void>;
83
83
  sendPing: () => Promise<void>;
84
- /** Sends a ping-frame and resolves with the round-trip time in milliseconds once the correlated pong-frame arrives -- FIFO-paired against this call's own ping, since ping-frame carries no correlation id of its own (spec/transport.cddl): the Nth call's own promise resolves against the Nth pong received after it, never matched by any other means. Rejects if the connection closes, or (when timeoutMs is given) if no pong arrives within timeoutMs, rather than resolving a sentinel value the way sendManageRequest's own timeout does -- there is no natural "no answer" value for a bare millisecond count to double as. Unlike sendPing (fire-and-forget, answered by nothing on an ordinary peer connection), this is answered only by a peer that replies to ping with pong -- today, relay-hub's own frame handling (wire-mesh#181) -- so calling this against a connection to a plain peer that never sends pong hangs until timeoutMs (if given) or forever. Exists to isolate the sender-to-hub leg of a relayed path.trace round trip: time this over the same connection a relayed manage-request travelled, then subtract it from path.trace's own end-to-end RTT to recover the hub-to-target leg. */
84
+ /**
85
+ * Sends a ping-frame and resolves with the round-trip time in milliseconds once the correlated pong-frame arrives.
86
+ *
87
+ * The pairing is FIFO because ping-frame carries no correlation id of its own (spec/transport.cddl): the Nth call's promise resolves against the Nth pong received after it, never matched by any other means.
88
+ *
89
+ * Rejects with a plain Error if the connection closes, and with a PingTimeoutError (from ping-round-trips.ts) when `timeoutMs` is given and no pong arrives within it, so a caller can tell an unanswered ping from a dropped connection. It never resolves a sentinel the way sendManageRequest's own timeout does, because there is no natural "no answer" value for a bare millisecond count to double as.
90
+ *
91
+ * Unlike sendPing (fire-and-forget, answered by nothing on an ordinary peer connection), this is answered only by a peer that replies to ping with pong, which today means relay-hub's own frame handling (wire-mesh#181). Called against a plain peer that never sends pong it hangs until `timeoutMs` if given, or forever.
92
+ *
93
+ * It exists to isolate the sender-to-hub leg of a relayed path.trace round trip: time it over the same connection a relayed manage-request travelled, then subtract it from path.trace's own end-to-end RTT to recover the hub-to-target leg.
94
+ */
85
95
  sendPingMeasureRtt: (timeoutMs?: number) => Promise<number>;
86
96
  /** Attaches this token to every `manage-request` sent from now on. */
87
97
  setToken: (token: CapabilityToken) => void;
@@ -10,7 +10,8 @@ import { RoomNoticeVerdictReason } from "./room.cjs";
10
10
  */
11
11
  export interface RoomKeyStore {
12
12
  get: (room: string, epoch: number) => Promise<Uint8Array | undefined>;
13
- set: (room: string, epoch: number, key: Uint8Array) => void;
13
+ /** May be asynchronous, as storing a key durably is; callers wait for it before relying on the key being held. */
14
+ set: (room: string, epoch: number, key: Uint8Array) => Promise<void> | void;
14
15
  /** The highest epoch held for a room -- what postEncryptedNotice stamps a new notice with. */
15
16
  currentEpoch: (room: string) => Promise<number | undefined>;
16
17
  }
@@ -10,7 +10,8 @@ import { RoomNoticeVerdictReason } from "./room.mjs";
10
10
  */
11
11
  export interface RoomKeyStore {
12
12
  get: (room: string, epoch: number) => Promise<Uint8Array | undefined>;
13
- set: (room: string, epoch: number, key: Uint8Array) => void;
13
+ /** May be asynchronous, as storing a key durably is; callers wait for it before relying on the key being held. */
14
+ set: (room: string, epoch: number, key: Uint8Array) => Promise<void> | void;
14
15
  /** The highest epoch held for a room -- what postEncryptedNotice stamps a new notice with. */
15
16
  currentEpoch: (room: string) => Promise<number | undefined>;
16
17
  }
@@ -1,5 +1,12 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/domain/ping-round-trips.ts
3
+ /** Why a ping was given up on: no pong arrived within the caller's `timeoutMs`. A call rejected for any other reason (the connection dropped or closed first) rejects with a plain Error, so a caller counting unanswered pings tells the two apart by this class rather than by message text. */
4
+ var PingTimeoutError = class extends Error {
5
+ constructor() {
6
+ super("timed out waiting for pong");
7
+ this.name = "PingTimeoutError";
8
+ }
9
+ };
3
10
  function createPingRoundTrips() {
4
11
  const pending = [];
5
12
  function cancel(sentAt) {
@@ -28,11 +35,12 @@ function createPingRoundTrips() {
28
35
  if (timeoutMs === void 0) return rtt;
29
36
  return Promise.race([rtt, new Promise((_resolve, reject) => {
30
37
  setTimeout(() => {
31
- if (cancel(sentAt)) reject(/* @__PURE__ */ new Error("timed out waiting for pong"));
38
+ if (cancel(sentAt)) reject(new PingTimeoutError());
32
39
  }, timeoutMs);
33
40
  })]);
34
41
  }
35
42
  };
36
43
  }
37
44
  //#endregion
45
+ exports.PingTimeoutError = PingTimeoutError;
38
46
  exports.createPingRoundTrips = createPingRoundTrips;
@@ -1,4 +1,8 @@
1
1
  //#region src/domain/ping-round-trips.d.ts
2
+ /** Why a ping was given up on: no pong arrived within the caller's `timeoutMs`. A call rejected for any other reason (the connection dropped or closed first) rejects with a plain Error, so a caller counting unanswered pings tells the two apart by this class rather than by message text. */
3
+ export declare class PingTimeoutError extends Error {
4
+ constructor();
5
+ }
2
6
  export interface PingRoundTrips {
3
7
  /** Resolves the oldest still-pending call with (nowMs - itsOwnSentAt) -- called once per pong-frame received. A no-op if nothing is pending (a stray pong with no outstanding call). */
4
8
  resolveOldest: (nowMs: number) => void;
@@ -1,4 +1,8 @@
1
1
  //#region src/domain/ping-round-trips.d.ts
2
+ /** Why a ping was given up on: no pong arrived within the caller's `timeoutMs`. A call rejected for any other reason (the connection dropped or closed first) rejects with a plain Error, so a caller counting unanswered pings tells the two apart by this class rather than by message text. */
3
+ export declare class PingTimeoutError extends Error {
4
+ constructor();
5
+ }
2
6
  export interface PingRoundTrips {
3
7
  /** Resolves the oldest still-pending call with (nowMs - itsOwnSentAt) -- called once per pong-frame received. A no-op if nothing is pending (a stray pong with no outstanding call). */
4
8
  resolveOldest: (nowMs: number) => void;
@@ -1,4 +1,11 @@
1
1
  //#region src/domain/ping-round-trips.ts
2
+ /** Why a ping was given up on: no pong arrived within the caller's `timeoutMs`. A call rejected for any other reason (the connection dropped or closed first) rejects with a plain Error, so a caller counting unanswered pings tells the two apart by this class rather than by message text. */
3
+ var PingTimeoutError = class extends Error {
4
+ constructor() {
5
+ super("timed out waiting for pong");
6
+ this.name = "PingTimeoutError";
7
+ }
8
+ };
2
9
  function createPingRoundTrips() {
3
10
  const pending = [];
4
11
  function cancel(sentAt) {
@@ -27,11 +34,11 @@ function createPingRoundTrips() {
27
34
  if (timeoutMs === void 0) return rtt;
28
35
  return Promise.race([rtt, new Promise((_resolve, reject) => {
29
36
  setTimeout(() => {
30
- if (cancel(sentAt)) reject(/* @__PURE__ */ new Error("timed out waiting for pong"));
37
+ if (cancel(sentAt)) reject(new PingTimeoutError());
31
38
  }, timeoutMs);
32
39
  })]);
33
40
  }
34
41
  };
35
42
  }
36
43
  //#endregion
37
- export { createPingRoundTrips };
44
+ export { PingTimeoutError, createPingRoundTrips };
@@ -100,7 +100,7 @@ function createRoomRekeyHandler(options) {
100
100
  return;
101
101
  }
102
102
  await incoming.respond({ result: "ok" });
103
- options.onRekey({
103
+ await options.onRekey({
104
104
  room: roomPath,
105
105
  keyEpoch,
106
106
  contentKeys
@@ -25,8 +25,8 @@ export interface CreateRoomRekeyHandlerOptions {
25
25
  revocation: RevocationCheck;
26
26
  /** This recipient's own currently-held room:member token for the room being rekeyed, verified fresh on every incoming room.rekey (not cached) -- see this module's own doc comment for why its certified root issuer-key is what the ECDH derivation uses, rather than a separately-tracked live-sender identity. */
27
27
  ownRoomMemberToken: CapabilityToken;
28
- /** Called once per successfully unwrapped room.rekey. */
29
- onRekey: (event: Readonly<RoomRekeyEvent>) => void;
28
+ /** Called once per successfully unwrapped room.rekey, after the sender has been told it succeeded. May be asynchronous, as storing a key durably is; the handler waits for it, so a failure to store reaches whoever awaits the handler. */
29
+ onRekey: (event: Readonly<RoomRekeyEvent>) => Promise<void> | void;
30
30
  }
31
31
  /**
32
32
  * Builds a reusable handler for one room's incoming room.rekey messages. Checks, in order: the outer verb is room:member (otherwise "malformed"); the params payload parses against room.rekey's own CDDL shape (otherwise "malformed"); the request's own scope is a room scope (otherwise "scope_mismatch"); ownRoomMemberToken independently passes every one of core/room's six verifier obligations, scoped to the incoming request's own room path (a failure responds with verifyRoomToken's own specific reason, e.g. "wrong_chain_root"/"wrong_scope_path"/"expired"); this identity actually supports deriveSharedSecret (otherwise "ecdh_unsupported" -- an Ed25519-only identity genuinely cannot participate). Only then does it derive the shared secret against the verified chain's own rootIssuerKey, derive one wrapping key per granted epoch (per the array-ordering convention: entry i is epoch i+1, counting back from key-epoch), and unwrap each -- a failure at that final cryptographic step (a forged sender, or genuine corruption) responds "unwrap_failed" rather than surfacing a partially-decoded result.
@@ -25,8 +25,8 @@ export interface CreateRoomRekeyHandlerOptions {
25
25
  revocation: RevocationCheck;
26
26
  /** This recipient's own currently-held room:member token for the room being rekeyed, verified fresh on every incoming room.rekey (not cached) -- see this module's own doc comment for why its certified root issuer-key is what the ECDH derivation uses, rather than a separately-tracked live-sender identity. */
27
27
  ownRoomMemberToken: CapabilityToken;
28
- /** Called once per successfully unwrapped room.rekey. */
29
- onRekey: (event: Readonly<RoomRekeyEvent>) => void;
28
+ /** Called once per successfully unwrapped room.rekey, after the sender has been told it succeeded. May be asynchronous, as storing a key durably is; the handler waits for it, so a failure to store reaches whoever awaits the handler. */
29
+ onRekey: (event: Readonly<RoomRekeyEvent>) => Promise<void> | void;
30
30
  }
31
31
  /**
32
32
  * Builds a reusable handler for one room's incoming room.rekey messages. Checks, in order: the outer verb is room:member (otherwise "malformed"); the params payload parses against room.rekey's own CDDL shape (otherwise "malformed"); the request's own scope is a room scope (otherwise "scope_mismatch"); ownRoomMemberToken independently passes every one of core/room's six verifier obligations, scoped to the incoming request's own room path (a failure responds with verifyRoomToken's own specific reason, e.g. "wrong_chain_root"/"wrong_scope_path"/"expired"); this identity actually supports deriveSharedSecret (otherwise "ecdh_unsupported" -- an Ed25519-only identity genuinely cannot participate). Only then does it derive the shared secret against the verified chain's own rootIssuerKey, derive one wrapping key per granted epoch (per the array-ordering convention: entry i is epoch i+1, counting back from key-epoch), and unwrap each -- a failure at that final cryptographic step (a forged sender, or genuine corruption) responds "unwrap_failed" rather than surfacing a partially-decoded result.
@@ -99,7 +99,7 @@ function createRoomRekeyHandler(options) {
99
99
  return;
100
100
  }
101
101
  await incoming.respond({ result: "ok" });
102
- options.onRekey({
102
+ await options.onRekey({
103
103
  room: roomPath,
104
104
  keyEpoch,
105
105
  contentKeys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "3.5.1",
3
+ "version": "3.6.1",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "1.0.1",