wire-mesh-core 1.53.0 → 1.55.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.
Files changed (75) hide show
  1. package/dist/adapters/frame-codec.d.cts +1 -1
  2. package/dist/adapters/frame-codec.d.mts +1 -1
  3. package/dist/adapters/node-identity.d.cts +2 -2
  4. package/dist/adapters/node-identity.d.mts +2 -2
  5. package/dist/adapters/tcp-transport.d.cts +1 -1
  6. package/dist/adapters/tcp-transport.d.mts +1 -1
  7. package/dist/adapters/threshold-identity.d.cts +2 -2
  8. package/dist/adapters/threshold-identity.d.mts +2 -2
  9. package/dist/adapters/threshold-wasm.d.cts +1 -1
  10. package/dist/adapters/threshold-wasm.d.mts +1 -1
  11. package/dist/adapters/tls-transport.d.cts +1 -1
  12. package/dist/adapters/tls-transport.d.mts +1 -1
  13. package/dist/domain/bulk.d.cts +2 -2
  14. package/dist/domain/bulk.d.mts +2 -2
  15. package/dist/domain/capability-grant.d.cts +3 -3
  16. package/dist/domain/capability-grant.d.mts +3 -3
  17. package/dist/domain/capability-request.d.cts +2 -2
  18. package/dist/domain/capability-request.d.mts +2 -2
  19. package/dist/domain/data-sync.d.cts +2 -2
  20. package/dist/domain/data-sync.d.mts +2 -2
  21. package/dist/domain/device-id.d.cts +1 -1
  22. package/dist/domain/device-id.d.mts +1 -1
  23. package/dist/domain/gossip-expansion.cjs +44 -0
  24. package/dist/domain/gossip-expansion.d.cts +29 -0
  25. package/dist/domain/gossip-expansion.d.mts +29 -0
  26. package/dist/domain/gossip-expansion.mjs +43 -0
  27. package/dist/domain/grant-candidates.d.cts +1 -1
  28. package/dist/domain/grant-candidates.d.mts +1 -1
  29. package/dist/domain/handshake.d.cts +1 -1
  30. package/dist/domain/handshake.d.mts +1 -1
  31. package/dist/domain/mesh-session.cjs +74 -13
  32. package/dist/domain/mesh-session.d.cts +10 -4
  33. package/dist/domain/mesh-session.d.mts +10 -4
  34. package/dist/domain/mesh-session.mjs +74 -13
  35. package/dist/domain/notice-board.d.cts +3 -3
  36. package/dist/domain/notice-board.d.mts +3 -3
  37. package/dist/domain/relay-hub.cjs +6 -0
  38. package/dist/domain/relay-hub.d.cts +3 -3
  39. package/dist/domain/relay-hub.d.mts +3 -3
  40. package/dist/domain/relay-hub.mjs +6 -0
  41. package/dist/domain/relay-use-gate.d.cts +2 -2
  42. package/dist/domain/relay-use-gate.d.mts +2 -2
  43. package/dist/domain/revocation-view.d.cts +2 -2
  44. package/dist/domain/revocation-view.d.mts +2 -2
  45. package/dist/domain/room-rekey.d.cts +3 -3
  46. package/dist/domain/room-rekey.d.mts +3 -3
  47. package/dist/domain/room-token-verification.d.cts +1 -1
  48. package/dist/domain/room-token-verification.d.mts +1 -1
  49. package/dist/domain/room.d.cts +4 -4
  50. package/dist/domain/room.d.mts +4 -4
  51. package/dist/domain/shard-manifest.d.cts +1 -1
  52. package/dist/domain/shard-manifest.d.mts +1 -1
  53. package/dist/domain/tokens.d.cts +1 -1
  54. package/dist/domain/tokens.d.mts +1 -1
  55. package/dist/domain/webrtc-signaling.d.cts +2 -2
  56. package/dist/domain/webrtc-signaling.d.mts +2 -2
  57. package/dist/generated/protocol.cjs +12 -0
  58. package/dist/generated/protocol.d.cts +2 -2
  59. package/dist/generated/protocol.d.mts +2 -2
  60. package/dist/generated/protocol.mjs +10 -1
  61. package/dist/{identity-9TmmK7vP.d.mts → identity-BXrJ6_YM.d.mts} +1 -1
  62. package/dist/{identity-CUkDHCRK.d.cts → identity-CmD4l_Mp.d.cts} +1 -1
  63. package/dist/ports/identity.d.cts +1 -1
  64. package/dist/ports/identity.d.mts +1 -1
  65. package/dist/ports/transport.d.cts +1 -1
  66. package/dist/ports/transport.d.mts +1 -1
  67. package/dist/{protocol-CNFRVx9w.d.cts → protocol-PhUVprXA.d.cts} +34 -6
  68. package/dist/{protocol-CNFRVx9w.d.mts → protocol-PhUVprXA.d.mts} +34 -6
  69. package/dist/{room-token-verification-BdtM1rQ4.d.mts → room-token-verification-D8HndWYM.d.cts} +2 -2
  70. package/dist/{room-token-verification-DpOfZOdl.d.cts → room-token-verification-Dv1O6-FV.d.mts} +2 -2
  71. package/dist/{tokens-CVyPoJ6l.d.mts → tokens-BVrgBeqF.d.mts} +2 -2
  72. package/dist/{tokens-Cl8sQo7o.d.cts → tokens-DIPzZ9JJ.d.cts} +2 -2
  73. package/dist/{transport-C7KVkhgb.d.mts → transport-CbXX1Xd-.d.cts} +1 -1
  74. package/dist/{transport-De0zHlaA.d.cts → transport-DG9ouGe-.d.mts} +1 -1
  75. package/package.json +5 -1
@@ -1,4 +1,4 @@
1
- import { D as DeviceId, N as Frame, ft as RelayDataFrame } from "../protocol-CNFRVx9w.cjs";
1
+ import { D as DeviceId, N as Frame, ht as RelayDataFrame } from "../protocol-PhUVprXA.cjs";
2
2
  //#region src/adapters/frame-codec.d.ts
3
3
  export declare function messageFromFrame(frame: Frame): Uint8Array<ArrayBuffer>;
4
4
  /** Wraps frame as a relay-data-frame's own opaque payload, stamping to-device when the caller knows which established pairing to address it to (wire-mesh#30) -- the outbound counterpart to tryDecodeFrame's own doc comment below, which describes the inbound side of the same relay-data envelope. Omitting toDevice leaves the frame unaddressed, which the receiving hub then routes via its own most-recently-established-pairing fallback. */
@@ -1,4 +1,4 @@
1
- import { D as DeviceId, N as Frame, ft as RelayDataFrame } from "../protocol-CNFRVx9w.mjs";
1
+ import { D as DeviceId, N as Frame, ht as RelayDataFrame } from "../protocol-PhUVprXA.mjs";
2
2
  //#region src/adapters/frame-codec.d.ts
3
3
  export declare function messageFromFrame(frame: Frame): Uint8Array<ArrayBuffer>;
4
4
  /** Wraps frame as a relay-data-frame's own opaque payload, stamping to-device when the caller knows which established pairing to address it to (wire-mesh#30) -- the outbound counterpart to tryDecodeFrame's own doc comment below, which describes the inbound side of the same relay-data envelope. Omitting toDevice leaves the frame unaddressed, which the receiving hub then routes via its own most-recently-established-pairing fallback. */
@@ -1,5 +1,5 @@
1
- import { B as IdentityKey, D as DeviceId } from "../protocol-CNFRVx9w.cjs";
2
- import { t as IdentityPort } from "../identity-CUkDHCRK.cjs";
1
+ import { B as IdentityKey, D as DeviceId } from "../protocol-PhUVprXA.cjs";
2
+ import { t as IdentityPort } from "../identity-CmD4l_Mp.cjs";
3
3
  import { webcrypto } from "node:crypto";
4
4
  //#region src/adapters/node-identity.d.ts
5
5
  export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
@@ -1,5 +1,5 @@
1
- import { B as IdentityKey, D as DeviceId } from "../protocol-CNFRVx9w.mjs";
2
- import { t as IdentityPort } from "../identity-9TmmK7vP.mjs";
1
+ import { B as IdentityKey, D as DeviceId } from "../protocol-PhUVprXA.mjs";
2
+ import { t as IdentityPort } from "../identity-BXrJ6_YM.mjs";
3
3
  import { webcrypto } from "node:crypto";
4
4
  //#region src/adapters/node-identity.d.ts
5
5
  export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-De0zHlaA.cjs";
1
+ import { r as Transport } from "../transport-CbXX1Xd-.cjs";
2
2
  //#region src/adapters/tcp-transport.d.ts
3
3
  /** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
4
4
  export declare function createTcpTransport(): Transport;
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-C7KVkhgb.mjs";
1
+ import { r as Transport } from "../transport-DG9ouGe-.mjs";
2
2
  //#region src/adapters/tcp-transport.d.ts
3
3
  /** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
4
4
  export declare function createTcpTransport(): Transport;
@@ -1,5 +1,5 @@
1
- import { D as DeviceId } from "../protocol-CNFRVx9w.cjs";
2
- import { t as IdentityPort } from "../identity-CUkDHCRK.cjs";
1
+ import { D as DeviceId } from "../protocol-PhUVprXA.cjs";
2
+ import { t as IdentityPort } from "../identity-CmD4l_Mp.cjs";
3
3
  import { DeviceKeyed } from "./threshold-wasm.cjs";
4
4
  import { ThresholdSubject } from "../domain/threshold-subject.cjs";
5
5
  //#region src/adapters/threshold-identity.d.ts
@@ -1,5 +1,5 @@
1
- import { D as DeviceId } from "../protocol-CNFRVx9w.mjs";
2
- import { t as IdentityPort } from "../identity-9TmmK7vP.mjs";
1
+ import { D as DeviceId } from "../protocol-PhUVprXA.mjs";
2
+ import { t as IdentityPort } from "../identity-BXrJ6_YM.mjs";
3
3
  import { DeviceKeyed } from "./threshold-wasm.mjs";
4
4
  import { ThresholdSubject } from "../domain/threshold-subject.mjs";
5
5
  //#region src/adapters/threshold-identity.d.ts
@@ -1,4 +1,4 @@
1
- import { D as DeviceId } from "../protocol-CNFRVx9w.cjs";
1
+ import { D as DeviceId } from "../protocol-PhUVprXA.cjs";
2
2
  //#region src/adapters/threshold-wasm.d.ts
3
3
  /** One `(deviceId, bytes)` pair -- the wasm boundary's own encoding of a map keyed by participant, since wasm-bindgen has no built-in `Map<Uint8Array, Uint8Array>` marshalling. */
4
4
  export interface DeviceKeyed<T extends Uint8Array = Uint8Array<ArrayBuffer>> {
@@ -1,4 +1,4 @@
1
- import { D as DeviceId } from "../protocol-CNFRVx9w.mjs";
1
+ import { D as DeviceId } from "../protocol-PhUVprXA.mjs";
2
2
  //#region src/adapters/threshold-wasm.d.ts
3
3
  /** One `(deviceId, bytes)` pair -- the wasm boundary's own encoding of a map keyed by participant, since wasm-bindgen has no built-in `Map<Uint8Array, Uint8Array>` marshalling. */
4
4
  export interface DeviceKeyed<T extends Uint8Array = Uint8Array<ArrayBuffer>> {
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-De0zHlaA.cjs";
1
+ import { r as Transport } from "../transport-CbXX1Xd-.cjs";
2
2
  //#region src/adapters/tls-transport.d.ts
3
3
  export interface TlsIdentity {
4
4
  certificatePem: string;
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-C7KVkhgb.mjs";
1
+ import { r as Transport } from "../transport-DG9ouGe-.mjs";
2
2
  //#region src/adapters/tls-transport.d.ts
3
3
  export interface TlsIdentity {
4
4
  certificatePem: string;
@@ -1,6 +1,6 @@
1
- import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-CNFRVx9w.cjs";
1
+ import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-PhUVprXA.cjs";
2
2
  import { KeyValueStorage } from "../ports/storage.cjs";
3
- import { t as Connection } from "../transport-De0zHlaA.cjs";
3
+ import { t as Connection } from "../transport-CbXX1Xd-.cjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
5
5
  //#region src/domain/bulk.d.ts
6
6
  /** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
@@ -1,6 +1,6 @@
1
- import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-CNFRVx9w.mjs";
1
+ import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-PhUVprXA.mjs";
2
2
  import { KeyValueStorage } from "../ports/storage.mjs";
3
- import { t as Connection } from "../transport-C7KVkhgb.mjs";
3
+ import { t as Connection } from "../transport-DG9ouGe-.mjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
5
  //#region src/domain/bulk.d.ts
6
6
  /** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
@@ -1,8 +1,8 @@
1
- import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-CNFRVx9w.cjs";
2
- import { t as IdentityPort } from "../identity-CUkDHCRK.cjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-PhUVprXA.cjs";
2
+ import { t as IdentityPort } from "../identity-CmD4l_Mp.cjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.cjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
5
- import { a as RevocationCheck } from "../tokens-Cl8sQo7o.cjs";
5
+ import { a as RevocationCheck } from "../tokens-DIPzZ9JJ.cjs";
6
6
  //#region src/domain/capability-grant.d.ts
7
7
  /**
8
8
  * Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
@@ -1,8 +1,8 @@
1
- import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-CNFRVx9w.mjs";
2
- import { t as IdentityPort } from "../identity-9TmmK7vP.mjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-PhUVprXA.mjs";
2
+ import { t as IdentityPort } from "../identity-BXrJ6_YM.mjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
5
- import { a as RevocationCheck } from "../tokens-CVyPoJ6l.mjs";
5
+ import { a as RevocationCheck } from "../tokens-BVrgBeqF.mjs";
6
6
  //#region src/domain/capability-grant.d.ts
7
7
  /**
8
8
  * Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
@@ -1,5 +1,5 @@
1
- import { D as DeviceId, V as ManageCommand, f as CapabilityScope, u as CapabilityGrantOk } from "../protocol-CNFRVx9w.cjs";
2
- import { t as IdentityPort } from "../identity-CUkDHCRK.cjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, u as CapabilityGrantOk } from "../protocol-PhUVprXA.cjs";
2
+ import { t as IdentityPort } from "../identity-CmD4l_Mp.cjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.cjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
5
5
  //#region src/domain/capability-request.d.ts
@@ -1,5 +1,5 @@
1
- import { D as DeviceId, V as ManageCommand, f as CapabilityScope, u as CapabilityGrantOk } from "../protocol-CNFRVx9w.mjs";
2
- import { t as IdentityPort } from "../identity-9TmmK7vP.mjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, u as CapabilityGrantOk } from "../protocol-PhUVprXA.mjs";
2
+ import { t as IdentityPort } from "../identity-BXrJ6_YM.mjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
5
  //#region src/domain/capability-request.d.ts
@@ -1,6 +1,6 @@
1
- import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-CNFRVx9w.cjs";
1
+ import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-PhUVprXA.cjs";
2
2
  import { KeyValueStorage } from "../ports/storage.cjs";
3
- import { t as IdentityPort } from "../identity-CUkDHCRK.cjs";
3
+ import { t as IdentityPort } from "../identity-CmD4l_Mp.cjs";
4
4
  //#region src/domain/data-sync.d.ts
5
5
  export interface DataSyncOptions {
6
6
  identity: IdentityPort;
@@ -1,6 +1,6 @@
1
- import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-CNFRVx9w.mjs";
1
+ import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-PhUVprXA.mjs";
2
2
  import { KeyValueStorage } from "../ports/storage.mjs";
3
- import { t as IdentityPort } from "../identity-9TmmK7vP.mjs";
3
+ import { t as IdentityPort } from "../identity-BXrJ6_YM.mjs";
4
4
  //#region src/domain/data-sync.d.ts
5
5
  export interface DataSyncOptions {
6
6
  identity: IdentityPort;
@@ -1,4 +1,4 @@
1
- import { D as DeviceId } from "../protocol-CNFRVx9w.cjs";
1
+ import { D as DeviceId } from "../protocol-PhUVprXA.cjs";
2
2
  //#region src/domain/device-id.d.ts
3
3
  /** Lowercase, byte-exact hex for an arbitrary-length byte string -- the same encoding convention deviceIdToHex uses for the fixed-length device-id case, extracted so any other byte string needing a stable, displayable, map-keyable text form (e.g. a token-id, which tokens.cddl defines as an arbitrary-length bstr rather than a 32-byte device-id) can use the identical convention without going through a device-id-shaped function. */
4
4
  export declare function bytesToHex(bytes: Uint8Array): string;
@@ -1,4 +1,4 @@
1
- import { D as DeviceId } from "../protocol-CNFRVx9w.mjs";
1
+ import { D as DeviceId } from "../protocol-PhUVprXA.mjs";
2
2
  //#region src/domain/device-id.d.ts
3
3
  /** Lowercase, byte-exact hex for an arbitrary-length byte string -- the same encoding convention deviceIdToHex uses for the fixed-length device-id case, extracted so any other byte string needing a stable, displayable, map-keyable text form (e.g. a token-id, which tokens.cddl defines as an arbitrary-length bstr rather than a 32-byte device-id) can use the identical convention without going through a device-id-shaped function. */
4
4
  export declare function bytesToHex(bytes: Uint8Array): string;
@@ -0,0 +1,44 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_domain_device_id = require("./device-id.cjs");
3
+ //#region src/domain/gossip-expansion.ts
4
+ function toError(value) {
5
+ return value instanceof Error ? value : new Error(String(value));
6
+ }
7
+ /** Builds the per-device expansion state machine: ask once, dial every advertised address in order on approval, report exactly one outcome. */
8
+ function createGossipExpansion(options) {
9
+ const selfHex = require_domain_device_id.deviceIdToHex(options.selfDeviceId);
10
+ const considered = /* @__PURE__ */ new Set();
11
+ async function expand(candidate) {
12
+ let approved;
13
+ try {
14
+ approved = await options.shouldExpand(candidate);
15
+ } catch (error) {
16
+ options.onExpansionFailed?.(candidate, [toError(error)]);
17
+ return;
18
+ }
19
+ if (!approved) {
20
+ options.onExpansionDeclined?.(candidate);
21
+ return;
22
+ }
23
+ const errors = [];
24
+ for (const address of candidate.addresses) try {
25
+ const session = await options.dial(address);
26
+ options.onExpanded(candidate, address, session);
27
+ return;
28
+ } catch (error) {
29
+ errors.push(toError(error));
30
+ }
31
+ options.onExpansionFailed?.(candidate, errors);
32
+ }
33
+ return { considerAdvert(advert) {
34
+ const hex = require_domain_device_id.deviceIdToHex(advert.device);
35
+ if (hex === selfHex || considered.has(hex) || advert.addresses.length === 0) return;
36
+ considered.add(hex);
37
+ expand({
38
+ device: advert.device,
39
+ addresses: advert.addresses
40
+ });
41
+ } };
42
+ }
43
+ //#endregion
44
+ exports.createGossipExpansion = createGossipExpansion;
@@ -0,0 +1,29 @@
1
+ import { D as DeviceId, et as PeerAdvert } from "../protocol-PhUVprXA.cjs";
2
+ import { MeshSession } from "./mesh-session.cjs";
3
+ //#region src/domain/gossip-expansion.d.ts
4
+ /** One not-yet-considered device this session's gossip directory has surfaced, carrying only what a dial/trust decision needs: the device-id to decide identity, and the "host:port" addresses its own peer-advert claims are directly reachable at, in the order it advertised them. */
5
+ export interface GossipExpansionCandidate {
6
+ readonly device: DeviceId;
7
+ readonly addresses: readonly string[];
8
+ }
9
+ export interface GossipExpansionOptions {
10
+ /** This session's own device-id, so its own self-advert (looped back through gossip forwarding or a hub's catch-up frame) is never offered to shouldExpand as if it were a distinct peer. */
11
+ selfDeviceId: DeviceId;
12
+ /** Decides whether to attempt a direct connection to a newly-discovered device at all -- called at most once per device for this GossipExpansion instance's lifetime, never re-asked if the same device later re-adverts (an address change, a fresher snapshot-seconds). Returning true does not guarantee a connection: dial may still fail for every one of the candidate's addresses, reported via onExpansionFailed. */
13
+ shouldExpand: (candidate: Readonly<GossipExpansionCandidate>) => Promise<boolean> | boolean;
14
+ /** Attempts one direct connection to `address`, resolving with a fully connected MeshSession or rejecting if the dial/handshake fails. Typically wraps createMeshSession(transport, identity, ...) followed by session.connect(address, localDomains). Called once per address of an approved candidate, in the order it advertised them, stopping at the first that succeeds -- the caller owns cleanup of any partially-established session behind a rejected dial, since this module never holds a reference to one that didn't resolve. */
15
+ dial: (address: string) => Promise<MeshSession>;
16
+ /** Called once a candidate's dial succeeds, with the specific address that connected (one of candidate.addresses, not necessarily the first -- earlier ones may have failed) and the now-connected session, so the caller can wire it into whatever it already does for its own sessions (render it, merge its directory, route messages). */
17
+ onExpanded: (candidate: Readonly<GossipExpansionCandidate>, address: string, session: MeshSession) => void;
18
+ /** Called when shouldExpand itself declines a candidate. */
19
+ onExpansionDeclined?: (candidate: Readonly<GossipExpansionCandidate>) => void;
20
+ /** Called once every one of an approved candidate's addresses has failed to connect, or shouldExpand itself rejected -- in the latter case, errors holds exactly the one rejection reason, so a caller doesn't need two separate failure channels to observe every way expansion can fail short of success. */
21
+ onExpansionFailed?: (candidate: Readonly<GossipExpansionCandidate>, errors: readonly Error[]) => void;
22
+ }
23
+ export interface GossipExpansion {
24
+ /** Feed one just-received peer-advert -- from the session whose peer-adverts this expansion watches, via its own onPeerAdvert hook -- to decide whether it names a newly-discoverable peer worth expanding to, and if so kick off an expansion attempt for it. Never awaits a dial to completion inline: expansion happens in the background, observable only through onExpanded/onExpansionDeclined/onExpansionFailed. */
25
+ considerAdvert: (advert: Readonly<PeerAdvert>) => void;
26
+ }
27
+ /** Builds the per-device expansion state machine: ask once, dial every advertised address in order on approval, report exactly one outcome. */
28
+ export declare function createGossipExpansion(options: Readonly<GossipExpansionOptions>): GossipExpansion;
29
+ //#endregion
@@ -0,0 +1,29 @@
1
+ import { D as DeviceId, et as PeerAdvert } from "../protocol-PhUVprXA.mjs";
2
+ import { MeshSession } from "./mesh-session.mjs";
3
+ //#region src/domain/gossip-expansion.d.ts
4
+ /** One not-yet-considered device this session's gossip directory has surfaced, carrying only what a dial/trust decision needs: the device-id to decide identity, and the "host:port" addresses its own peer-advert claims are directly reachable at, in the order it advertised them. */
5
+ export interface GossipExpansionCandidate {
6
+ readonly device: DeviceId;
7
+ readonly addresses: readonly string[];
8
+ }
9
+ export interface GossipExpansionOptions {
10
+ /** This session's own device-id, so its own self-advert (looped back through gossip forwarding or a hub's catch-up frame) is never offered to shouldExpand as if it were a distinct peer. */
11
+ selfDeviceId: DeviceId;
12
+ /** Decides whether to attempt a direct connection to a newly-discovered device at all -- called at most once per device for this GossipExpansion instance's lifetime, never re-asked if the same device later re-adverts (an address change, a fresher snapshot-seconds). Returning true does not guarantee a connection: dial may still fail for every one of the candidate's addresses, reported via onExpansionFailed. */
13
+ shouldExpand: (candidate: Readonly<GossipExpansionCandidate>) => Promise<boolean> | boolean;
14
+ /** Attempts one direct connection to `address`, resolving with a fully connected MeshSession or rejecting if the dial/handshake fails. Typically wraps createMeshSession(transport, identity, ...) followed by session.connect(address, localDomains). Called once per address of an approved candidate, in the order it advertised them, stopping at the first that succeeds -- the caller owns cleanup of any partially-established session behind a rejected dial, since this module never holds a reference to one that didn't resolve. */
15
+ dial: (address: string) => Promise<MeshSession>;
16
+ /** Called once a candidate's dial succeeds, with the specific address that connected (one of candidate.addresses, not necessarily the first -- earlier ones may have failed) and the now-connected session, so the caller can wire it into whatever it already does for its own sessions (render it, merge its directory, route messages). */
17
+ onExpanded: (candidate: Readonly<GossipExpansionCandidate>, address: string, session: MeshSession) => void;
18
+ /** Called when shouldExpand itself declines a candidate. */
19
+ onExpansionDeclined?: (candidate: Readonly<GossipExpansionCandidate>) => void;
20
+ /** Called once every one of an approved candidate's addresses has failed to connect, or shouldExpand itself rejected -- in the latter case, errors holds exactly the one rejection reason, so a caller doesn't need two separate failure channels to observe every way expansion can fail short of success. */
21
+ onExpansionFailed?: (candidate: Readonly<GossipExpansionCandidate>, errors: readonly Error[]) => void;
22
+ }
23
+ export interface GossipExpansion {
24
+ /** Feed one just-received peer-advert -- from the session whose peer-adverts this expansion watches, via its own onPeerAdvert hook -- to decide whether it names a newly-discoverable peer worth expanding to, and if so kick off an expansion attempt for it. Never awaits a dial to completion inline: expansion happens in the background, observable only through onExpanded/onExpansionDeclined/onExpansionFailed. */
25
+ considerAdvert: (advert: Readonly<PeerAdvert>) => void;
26
+ }
27
+ /** Builds the per-device expansion state machine: ask once, dial every advertised address in order on approval, report exactly one outcome. */
28
+ export declare function createGossipExpansion(options: Readonly<GossipExpansionOptions>): GossipExpansion;
29
+ //#endregion
@@ -0,0 +1,43 @@
1
+ import { deviceIdToHex } from "./device-id.mjs";
2
+ //#region src/domain/gossip-expansion.ts
3
+ function toError(value) {
4
+ return value instanceof Error ? value : new Error(String(value));
5
+ }
6
+ /** Builds the per-device expansion state machine: ask once, dial every advertised address in order on approval, report exactly one outcome. */
7
+ function createGossipExpansion(options) {
8
+ const selfHex = deviceIdToHex(options.selfDeviceId);
9
+ const considered = /* @__PURE__ */ new Set();
10
+ async function expand(candidate) {
11
+ let approved;
12
+ try {
13
+ approved = await options.shouldExpand(candidate);
14
+ } catch (error) {
15
+ options.onExpansionFailed?.(candidate, [toError(error)]);
16
+ return;
17
+ }
18
+ if (!approved) {
19
+ options.onExpansionDeclined?.(candidate);
20
+ return;
21
+ }
22
+ const errors = [];
23
+ for (const address of candidate.addresses) try {
24
+ const session = await options.dial(address);
25
+ options.onExpanded(candidate, address, session);
26
+ return;
27
+ } catch (error) {
28
+ errors.push(toError(error));
29
+ }
30
+ options.onExpansionFailed?.(candidate, errors);
31
+ }
32
+ return { considerAdvert(advert) {
33
+ const hex = deviceIdToHex(advert.device);
34
+ if (hex === selfHex || considered.has(hex) || advert.addresses.length === 0) return;
35
+ considered.add(hex);
36
+ expand({
37
+ device: advert.device,
38
+ addresses: advert.addresses
39
+ });
40
+ } };
41
+ }
42
+ //#endregion
43
+ export { createGossipExpansion };
@@ -1,4 +1,4 @@
1
- import { D as DeviceId, f as CapabilityScope } from "../protocol-CNFRVx9w.cjs";
1
+ import { D as DeviceId, f as CapabilityScope } from "../protocol-PhUVprXA.cjs";
2
2
  import { DirectoryEntry, MeshSession } from "./mesh-session.cjs";
3
3
  import { z } from "zod";
4
4
  //#region src/domain/grant-candidates.d.ts
@@ -1,4 +1,4 @@
1
- import { D as DeviceId, f as CapabilityScope } from "../protocol-CNFRVx9w.mjs";
1
+ import { D as DeviceId, f as CapabilityScope } from "../protocol-PhUVprXA.mjs";
2
2
  import { DirectoryEntry, MeshSession } from "./mesh-session.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/domain/grant-candidates.d.ts
@@ -1,4 +1,4 @@
1
- import { A as DomainId, R as HandshakeFrame, ot as ProtocolVersion } from "../protocol-CNFRVx9w.cjs";
1
+ import { A as DomainId, R as HandshakeFrame, lt as ProtocolVersion } from "../protocol-PhUVprXA.cjs";
2
2
  //#region src/domain/handshake.d.ts
3
3
  /** The highest protocol version this build of core understands. */
4
4
  export declare const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion;
@@ -1,4 +1,4 @@
1
- import { A as DomainId, R as HandshakeFrame, ot as ProtocolVersion } from "../protocol-CNFRVx9w.mjs";
1
+ import { A as DomainId, R as HandshakeFrame, lt as ProtocolVersion } from "../protocol-PhUVprXA.mjs";
2
2
  //#region src/domain/handshake.d.ts
3
3
  /** The highest protocol version this build of core understands. */
4
4
  export declare const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion;
@@ -13,8 +13,43 @@ function createRelayPairings() {
13
13
  };
14
14
  }
15
15
  //#endregion
16
- //#region src/domain/mesh-session.ts
17
- const MS_PER_SECOND = 1e3;
16
+ //#region src/domain/ping-round-trips.ts
17
+ function createPingRoundTrips() {
18
+ const pending = [];
19
+ function cancel(sentAt) {
20
+ const index = pending.findIndex((entry) => entry.sentAt === sentAt);
21
+ if (index === -1) return false;
22
+ pending.splice(index, 1);
23
+ return true;
24
+ }
25
+ return {
26
+ resolveOldest(nowMs) {
27
+ const entry = pending.shift();
28
+ if (entry !== void 0) entry.resolve(nowMs - entry.sentAt);
29
+ },
30
+ rejectAll(reason) {
31
+ for (const entry of pending.splice(0)) entry.reject(new Error(reason));
32
+ },
33
+ async sendAndAwait(sentAt, sendPing, timeoutMs) {
34
+ const rtt = new Promise((resolve, reject) => {
35
+ pending.push({
36
+ sentAt,
37
+ resolve,
38
+ reject
39
+ });
40
+ });
41
+ await sendPing();
42
+ if (timeoutMs === void 0) return rtt;
43
+ return Promise.race([rtt, new Promise((_resolve, reject) => {
44
+ setTimeout(() => {
45
+ if (cancel(sentAt)) reject(/* @__PURE__ */ new Error("timed out waiting for pong"));
46
+ }, timeoutMs);
47
+ })]);
48
+ }
49
+ };
50
+ }
51
+ //#endregion
52
+ //#region src/domain/gossip-extensions.ts
18
53
  /** peer-advert's own three typed fields -- reserved so a `sendGossipUpdate` caller can never override the session's own device-id, address list, or freshness timestamp by supplying an extension of the same name. */
19
54
  const RESERVED_PEER_ADVERT_KEYS = /* @__PURE__ */ new Set([
20
55
  "device",
@@ -30,6 +65,9 @@ function validateGossipExtensions(extensions) {
30
65
  if (!GOSSIP_EXTENSION_KEY_PATTERN.test(key)) throw new Error(`sendGossipUpdate extension key "${key}" must be domain-qualified as "<domain>/<field>" (e.g. "presence/status")`);
31
66
  }
32
67
  }
68
+ //#endregion
69
+ //#region src/domain/mesh-session.ts
70
+ const MS_PER_SECOND = 1e3;
33
71
  /** 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. */
34
72
  const HANDSHAKE_TIMEOUT_MS = 3e3;
35
73
  function localHandshake(domains) {
@@ -62,6 +100,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
62
100
  const incomingBacklog = [];
63
101
  const revocationWaiters = [];
64
102
  const revocationBacklog = [];
103
+ const pingRoundTrips = createPingRoundTrips();
65
104
  function snapshot() {
66
105
  return {
67
106
  state,
@@ -101,6 +140,11 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
101
140
  ...token !== null ? { token } : {}
102
141
  };
103
142
  }
143
+ /** The "not connected" guard every send* method on the public session needs, deduplicated into one place rather than repeated inline at each call site: throws unless there is a live connection AND the session's own state machine agrees it is "connected" (transmit's own connection===null check alone doesn't cover the latter). Returns the connection itself, narrowed non-null, for a caller that needs to send directly on it (sendPing/sendPingMeasureRtt); a caller that only needs the throwing side effect (sendManageRequest and friends, which go through transmit instead) calls this and discards the result. */
144
+ function requireConnectedLink() {
145
+ if (connection === null || state.status !== "connected") throw new Error("not connected");
146
+ return connection;
147
+ }
104
148
  /** Sends a frame, wrapping it as relay-data first when viaRelay is set -- the single choke point every outbound manage-request/manage-response passes through, so a consumer of sendManageRequest/respond never needs its own relay-wrapping logic. When relaying, toDevice is stamped onto the outer relay-data-frame's own `to-device` field so the hub addresses it to the correct pairing directly (wire-mesh#30) rather than falling back to whichever pairing it last saw -- the one case this is omitted is a response to a request that itself arrived with no from-device to echo back, which is left to that same hub fallback exactly as an unaddressed relay-data always has been. */
105
149
  async function transmit(frame, viaRelay, toDevice) {
106
150
  if (connection === null) throw new Error("not connected");
@@ -178,6 +222,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
178
222
  else if (frame.type === "relay-inbound") relayPairings.add(frame["source-device"]);
179
223
  else if (frame.type === "manage-response") applyManageResponse(frame);
180
224
  else if (frame.type === "manage-request") applyManageRequest(frame);
225
+ else if (frame.type === "pong") pingRoundTrips.resolveOldest(clock.now());
181
226
  else if (frame.type === "revocation-announce") for (const entry of frame.entries) emitRevocationEntry(entry);
182
227
  }
183
228
  /** Establishes a relay-connect pairing to targetDevice if this session isn't already paired with it -- a no-op when it already is, whether that pairing was established by this session's own prior relay-connect (initiator role) or learned from an incoming relay-inbound (target role, replying back to whoever dialed it). Pairing with a new target never tears down an existing pairing with a different one: this connection can hold several simultaneously (wire-mesh#30's own multiplexed adjacency map), so a later request back to an already-paired target must not re-send relay-connect for it. relay-connect has no ack frame: the initiator proceeds to relay-data right after sending it. */
@@ -220,6 +265,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
220
265
  function handleDisconnect(reason, address, localDomains) {
221
266
  if (feedCancelled) return;
222
267
  rejectPendingManageRequests("disconnected before a response arrived");
268
+ pingRoundTrips.rejectAll("disconnected before a pong arrived");
223
269
  if (reconnect !== null && attempt < reconnect.maxAttempts) {
224
270
  attempt += 1;
225
271
  const currentAttempt = attempt;
@@ -378,20 +424,32 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
378
424
  await doConnect(address, localDomains);
379
425
  },
380
426
  async sendPing() {
381
- if (connection === null || state.status !== "connected") throw new Error("not connected");
427
+ const link = requireConnectedLink();
382
428
  const ping = { type: "ping" };
383
429
  frameLog.push({
384
430
  direction: "sent",
385
431
  frame: ping
386
432
  });
387
- await connection.send(ping);
433
+ await link.send(ping);
388
434
  emit();
389
435
  },
436
+ async sendPingMeasureRtt(timeoutMs) {
437
+ const link = requireConnectedLink();
438
+ return pingRoundTrips.sendAndAwait(clock.now(), async () => {
439
+ const ping = { type: "ping" };
440
+ frameLog.push({
441
+ direction: "sent",
442
+ frame: ping
443
+ });
444
+ await link.send(ping);
445
+ emit();
446
+ }, timeoutMs);
447
+ },
390
448
  setToken(token) {
391
449
  currentToken = token;
392
450
  },
393
451
  async sendManageRequest(command, scope, targetDevice, token, timeoutMs) {
394
- if (connection === null || state.status !== "connected") throw new Error("not connected");
452
+ requireConnectedLink();
395
453
  if (targetDevice !== void 0) await ensureRelayPairing(targetDevice);
396
454
  const frame = buildManageRequest(command, scope, token);
397
455
  const requestId = frame["request-id"];
@@ -418,7 +476,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
418
476
  })]);
419
477
  },
420
478
  async sendRevocationAnnounce(entries) {
421
- if (connection === null || state.status !== "connected") throw new Error("not connected");
479
+ requireConnectedLink();
422
480
  const frame = {
423
481
  type: "revocation-announce",
424
482
  entries: [...entries]
@@ -431,7 +489,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
431
489
  emit();
432
490
  },
433
491
  async sendGossipUpdate(extensions) {
434
- if (connection === null || state.status !== "connected") throw new Error("not connected");
492
+ requireConnectedLink();
435
493
  const frame = buildSelfAdvert(extensions);
436
494
  frameLog.push({
437
495
  direction: "sent",
@@ -441,7 +499,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
441
499
  emit();
442
500
  },
443
501
  async sendDataFrame(frame) {
444
- if (connection === null || state.status !== "connected") throw new Error("not connected");
502
+ requireConnectedLink();
445
503
  frameLog.push({
446
504
  direction: "sent",
447
505
  frame
@@ -460,6 +518,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
460
518
  reconnectTimer = null;
461
519
  }
462
520
  rejectPendingManageRequests("connection closed before a response arrived");
521
+ pingRoundTrips.rejectAll("connection closed before a pong arrived");
463
522
  if (connection !== null) await connection.close();
464
523
  if (state.status === "connected" || state.status === "connecting" || state.status === "reconnecting") state = {
465
524
  status: "closed",
@@ -471,8 +530,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
471
530
  }
472
531
  };
473
532
  }
474
- function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = []) {
475
- const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), void 0, addresses);
533
+ function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert) {
534
+ const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses);
476
535
  return session;
477
536
  }
478
537
  /** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
@@ -484,9 +543,11 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
484
543
  });
485
544
  let peerDeviceIdResolved = false;
486
545
  const { session, wireUpConnection } = createSessionCore(identity, clock, null, null, (advert) => {
487
- if (peerDeviceIdResolved) return;
488
- peerDeviceIdResolved = true;
489
- resolvePeerDeviceId?.(advert.device);
546
+ if (!peerDeviceIdResolved) {
547
+ peerDeviceIdResolved = true;
548
+ resolvePeerDeviceId?.(advert.device);
549
+ }
550
+ options.onPeerAdvert?.(advert);
490
551
  }, options.addresses, options.onFrame, options.onSessionEnd);
491
552
  await wireUpConnection(connection, options.label ?? "accepted", localDomains);
492
553
  return {