wire-mesh-core 4.2.0 → 4.2.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.
@@ -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.mjs";
2
- import { IncomingManageRequest } from "./mesh-session.mjs";
2
+ import { d as IncomingManageRequest } from "../mesh-session-DzcZYdhe.mjs";
3
3
  import { VerifyCapabilityTokenOptions } from "./tokens.mjs";
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. */
@@ -0,0 +1,174 @@
1
+ import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "./protocol-Cb3BnuTL.cjs";
2
+ import { r as Transport, t as Connection } from "./transport-DaeS9N-o.cjs";
3
+ import { r as IdentityPort } from "./identity-CQof0iEM.cjs";
4
+ import { t as Clock } from "./clock-DhUPRfLh.cjs";
5
+ //#region src/domain/local-fault.d.ts
6
+ /** What `onFault` is told besides the error. */
7
+ interface FaultContext {
8
+ /** The peer the failing hello or send was for, when it is known by then. */
9
+ readonly peer: DeviceId | undefined;
10
+ /** How many reports for this peer were withheld since the last one: the shared fault reporter makes at most one report per peer per interval and counts the rest. */
11
+ readonly suppressed: number;
12
+ }
13
+ /** What the owner of a session hears of failures local to this side: the error, and which peer it was for and how many reports for that peer were withheld. */
14
+ type LocalFaultHandler = (error: unknown, context: FaultContext) => void | Promise<void>;
15
+ //#endregion
16
+ //#region src/domain/mesh-session.d.ts
17
+ /** 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. */
18
+ declare const HANDSHAKE_TIMEOUT_MS = 3000;
19
+ type ConnectionState = {
20
+ status: "idle";
21
+ } | {
22
+ status: "connecting";
23
+ address: string;
24
+ } | {
25
+ status: "connected";
26
+ address: string;
27
+ handshake: HandshakeStatus;
28
+ } | {
29
+ status: "reconnecting";
30
+ address: string;
31
+ attempt: number;
32
+ reason: string;
33
+ } | {
34
+ status: "closed";
35
+ address: string;
36
+ reason: string;
37
+ };
38
+ /** Opt-in retry behaviour for a session's own reconnect attempts. Left at its default `null` in createMeshSession, a disconnect always falls straight to `"closed"` -- today's exact behaviour, unchanged unless a caller opts in. */
39
+ interface ReconnectPolicy {
40
+ maxAttempts: number;
41
+ /** Backoff before attempt N (1-indexed), in milliseconds. */
42
+ delayMs: (attempt: number) => number;
43
+ }
44
+ /** The union `manage-response-frame.outcome` actually carries -- no dedicated generated type name exists for it. */
45
+ type ManageOutcome = ManageOk | ManageError;
46
+ /** One `manage-request` this session received from its peer, surfaced for the caller to act on and answer. */
47
+ interface IncomingManageRequest {
48
+ requestId: number;
49
+ command: ManageCommand;
50
+ scope: CapabilityScope;
51
+ token?: CapabilityToken;
52
+ /** The device-id of the peer this request was relayed on behalf of, as the end-to-end secure channel it arrived through authenticated it (spec/secure-channel.cddl): the identity that channel's handshake proved, never the `from-device` a hub stamps on relay-data, which a hub can set to anything. Present only when the request arrived through a relay pairing. A caller that needs to address a further request back to the same peer (one not sent via respond(), which already routes back correctly on its own) passes this as sendManageRequest's targetDevice. */
53
+ fromDevice?: DeviceId;
54
+ /** The device-id this request's relay-data frame was explicitly addressed to, read from its own `to-device` field -- present only when the request arrived relay-wrapped and the frame carried one. A caller fronting more than one locally-addressable device behind a single hub connection (a gateway advertising several local peers through the same relay pairing) uses this to decide whether the request is for this device or should be routed on to a different local peer it also advertises; this session has no such routing logic of its own, since it represents exactly one identity. */
55
+ toDevice?: DeviceId;
56
+ respond: (outcome: ManageOutcome) => Promise<void>;
57
+ }
58
+ /** The core/data frames: the raw have, request and entries primitive data-sync.ts answers. */
59
+ type DataDomainFrame = DataHaveFrame | DataRequestFrame | DataEntriesFrame;
60
+ /** One core/data frame this session received, surfaced for the application's replication policy to answer. */
61
+ interface IncomingDataFrame {
62
+ frame: DataDomainFrame;
63
+ /** The device-id of the peer that sent this frame through a relay pairing, as the end-to-end secure channel it arrived through authenticated it (spec/secure-channel.cddl), never the `from-device` a hub stamps. Absent for a frame that arrived directly on the connection, whose sender is the connection's own peer. A reply goes back to this device through sendDataFrame's targetDevice. */
64
+ fromDevice?: DeviceId;
65
+ /** The device-id the frame's relay-data was addressed to, for a caller fronting several local devices behind one hub connection, exactly as IncomingManageRequest.toDevice. Present only for a relayed frame that carried one. */
66
+ toDevice?: DeviceId;
67
+ }
68
+ type HandshakeStatus = {
69
+ status: "pending";
70
+ } | {
71
+ status: "negotiated";
72
+ version: ProtocolVersion;
73
+ sharedDomains: string[];
74
+ } | {
75
+ status: "unanswered";
76
+ } | {
77
+ status: "rejected";
78
+ reason: string;
79
+ };
80
+ interface SessionEvent {
81
+ state: ConnectionState;
82
+ /** The peer directory as of this event: latest peer-advert per device-id, in first-heard order. */
83
+ directory: readonly DirectoryEntry[];
84
+ /** The most recent frames that crossed the connection, sent or received, in order: at most FRAME_LOG_WINDOW entries, the recent-activity window a console shows rather than a session history. */
85
+ frameLog: readonly FrameLogEntry[];
86
+ }
87
+ interface DirectoryEntry {
88
+ device: DeviceId;
89
+ advert: PeerAdvert;
90
+ }
91
+ /**
92
+ * How many entries a session's frameLog keeps: the log exists for a console's
93
+ * recent-activity view, not as a session history, so it holds the most recent
94
+ * window of frames rather than every frame a long-lived session ever crossed.
95
+ * Without the bound, a busy session (a relay hub pairing, or any peer on a
96
+ * chatty mesh) retains every frame for its whole life, which on an
97
+ * always-on node grows without limit.
98
+ */
99
+ declare const FRAME_LOG_WINDOW = 256;
100
+ interface FrameLogEntry {
101
+ direction: "sent" | "received";
102
+ frame: Frame;
103
+ }
104
+ interface MeshSession {
105
+ readonly events: AsyncIterable<SessionEvent>;
106
+ /** Every `manage-request` received from the peer, in arrival order. */
107
+ readonly incomingManageRequests: AsyncIterable<IncomingManageRequest>;
108
+ /** Every core/data frame received, direct or through a relay pairing, in arrival order. A relayed one arrives only when it opened under the secure channel of the device that sent it, and names that device in `fromDevice`. What a frame means, and what to answer, is data-sync.ts's and the application's business, so the session only delivers it. */
109
+ readonly incomingDataFrames: AsyncIterable<IncomingDataFrame>;
110
+ /** Every `revocation-entry` received from the peer, in arrival order -- a `revocation-announce` frame's own `entries` array is flattened to one item per entry, since each entry is independently verifiable and independently meaningful regardless of which frame carried it. A consumer typically feeds each one into a RevocationView's own `record`. */
111
+ readonly revocationAnnouncements: AsyncIterable<RevocationEntry>;
112
+ /** Every `coordinator-frame` received from the peer, in arrival order: a gossiped, term-based claim to the rendezvous role (spec/transport.cddl), surfaced raw exactly like revocationAnnouncements because what a claim means is coordinator-election.ts's own business, not the session's. A consumer feeds each one into a CoordinatorElection's evaluate, and sends its own claims with sendCoordinatorClaim. */
113
+ readonly coordinatorFrames: AsyncIterable<CoordinatorFrame>;
114
+ connect: (address: string, localDomains: readonly string[]) => Promise<void>;
115
+ sendPing: () => Promise<void>;
116
+ /**
117
+ * Sends a ping-frame and resolves with the round-trip time in milliseconds once the correlated pong-frame arrives.
118
+ *
119
+ * 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.
120
+ *
121
+ * 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.
122
+ *
123
+ * 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.
124
+ *
125
+ * 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.
126
+ */
127
+ sendPingMeasureRtt: (timeoutMs?: number) => Promise<number>;
128
+ /** Attaches this token to every `manage-request` sent from now on. */
129
+ setToken: (token: CapabilityToken) => void;
130
+ /** Announces one or more already-minted revocation-entries to the peer. Sent directly over the connection, never relay-wrapped -- revocation-announce is a gossiped broadcast, not a request addressed to a specific peer, so it has no targetDevice/token parameters the way sendManageRequest does. */
131
+ sendRevocationAnnounce: (entries: readonly RevocationEntry[]) => Promise<void>;
132
+ /** Sends one coordinator-frame directly over this session's own connection, never relay-wrapped: the claim is a gossiped broadcast like revocation-announce, not a request addressed to a specific peer. The frame itself comes from a CoordinatorElection (claim, or announceCurrent for a refresh), so the session never invents a term on its own. */
133
+ sendCoordinatorClaim: (frame: Readonly<CoordinatorFrame>) => Promise<void>;
134
+ /** Re-sends this side's own self-advert with a fresh snapshot-seconds and, when given, extensions merged onto peer-advert's own open `* tstr => any` tail -- the mechanism a caller uses to keep gossiped presence status (or any other advertised fact) live over a connection's lifetime, since the initial self-advert wireUpConnection sends at connect time is otherwise never repeated. Callers own their own re-advertisement cadence (there is no timer inside MeshSession itself, matching its own DOM-free, fully unit-testable design); a caller not calling this again after connecting is exactly today's existing gossip-once-on-connect behaviour. */
135
+ sendGossipUpdate: (extensions?: Record<string, unknown>) => Promise<void>;
136
+ /** This session's own current topology snapshot -- the identical `topology/peers` value buildSelfAdvert merges into every gossiped self-advert (wire-mesh#180), read back directly rather than only from a possibly-stale gossiped copy elsewhere in the mesh. The live, cache-bust half of the cached-vs-live split `topology.get` itself establishes: a caller answering an incoming topology.get manage-request (see topology.ts's createTopologyGetHandler) calls this to build the response. Synchronous and side-effect-free -- unlike every other method here, it sends nothing and works even before this session has ever connected (an unconnected session simply has no direct peer and no relay pairings yet, both honestly empty). */
137
+ getTopologyPeers: () => TopologyPeers;
138
+ /** Sends one core/data frame (data-have, data-request, or data-entries): the transport half of an application's own noticeboard replication policy (data-sync.ts owns what the frames MEAN; this owns getting one onto the wire), the same layering sendRevocationAnnounce already established for its own frame kind. Without targetDevice it goes directly over this session's own connection, never relay-wrapped, which is how a hub's mailbox is addressed. With targetDevice it is routed to that specific peer through a relay pairing, sealed on the secure channel with that device like a manage-request (spec/secure-channel.cddl), so two devices that reach each other only through a hub can run the data-domain exchange; the hub sees ciphertext and its mailbox is not involved. The peer's own frames come back through incomingDataFrames. Rejects when not connected, exactly like every other send method here. */
139
+ sendDataFrame: (frame: DataDomainFrame, targetDevice?: DeviceId) => Promise<void>;
140
+ /** Sends a manage-request and resolves with the matching manage-response's outcome, correlated by request-id. When targetDevice is given, the request is routed to that specific peer via an established relay-connect pairing (wrapped as relay-data) rather than sent directly over this session's own Connection -- relay-hub deliberately drops manage-request/manage-response frames sent to it directly, since routing between two connected peers is not the relay role's business, so a specific peer reachable only through a relay hub can only be addressed this way. Absent, this sends directly over the Connection exactly as before. When token is given, it is attached to this one request instead of whatever setToken last set -- a single session routinely needs a different token per request when its peer shares more than one scope with this side (e.g. several core/room memberships over one connection), and a session-global token can only ever be correct for one of them. Absent, this request carries setToken's own session-global token exactly as before. When timeoutMs is given, the returned promise resolves with `{ result: "error", code: "timeout" }` rather than hanging forever if no manage-response arrives in time -- a held-open request (a human approval, a not-yet-online peer) otherwise has no way for the caller to give up on it. Absent, this request waits exactly as before, with no time limit of its own. */
141
+ sendManageRequest: (command: ManageCommand, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, token?: CapabilityToken, timeoutMs?: number) => Promise<ManageOutcome>;
142
+ close: () => Promise<void>;
143
+ }
144
+ declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null,
145
+ /** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
146
+ addresses?: readonly string[],
147
+ /** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- the hook a gossip-expansion consumer (wire-mesh#187, gossip-expansion.ts's own createGossipExpansion) uses to observe newly-gossiped peers without becoming a second, competing consumer of this session's own single-reader events stream (each emitted SessionEvent wakes at most one waiter, so a second for-await loop over events would silently steal events from whichever consumer already reads it). Omit for a caller with no use for it, exactly today's behaviour. */
148
+ onPeerAdvert?: (advert: PeerAdvert) => void,
149
+ /** 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. */
150
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
151
+ /** 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. Reports go through the shared fault reporter (fault-report.ts): at most one per peer per its interval, with the number withheld in between given as `suppressed` in the second argument, and at most its budget across all peers per interval, so a failure repeated for one peer is reported once and then counted. If the callback throws, or returns a promise that rejects, the reporter reports that to the console and swallows it. A report held back at the end of an interval is delivered when the interval ends, which can be after the session has closed. Omit for a caller with no use for it. */
152
+ onLocalFault?: LocalFaultHandler): MeshSession;
153
+ /** 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. */
154
+ interface AcceptedMeshSession extends MeshSession {
155
+ /** 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. */
156
+ peerDeviceId: Promise<DeviceId>;
157
+ }
158
+ interface AcceptedMeshSessionOptions {
159
+ /** A caller-chosen label for this connection, used only for ConnectionState's own address field -- the Connection/Transport ports expose no remote-address concept an accepted connection could report on its own (see wire-mesh#45). Defaults to a fixed placeholder since most callers have nothing more specific to offer; a transport adapter that does know the remote's address should pass it here. */
160
+ label?: string;
161
+ clock?: Readonly<Clock>;
162
+ /** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert. Omit (or pass none) for a caller with nothing to offer. */
163
+ addresses?: readonly string[];
164
+ /** Observes every frame this session receives, alongside (never instead of) its own manage-request/gossip/handshake handling (wire-mesh#102) -- the integration point an "ordinary opted-in node relays for peers it's already talking to" uses: pass a shared RelayHub instance's own handleFrame, bound to this connection, and it sees the identical frame stream this session's own applyFrame already consumes, with no second for-await loop racing over the same connection.receive(). A dedicated wire-mesh/cloudflare-hub deployment, which never runs a MeshSession at all, is unaffected -- this is additive, opt-in, and irrelevant to that case. */
165
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>;
166
+ /** Fired once this session's own connection.receive() stream ends -- the counterpart a RelayHub's own forgetConnection needs, since its registry is keyed by this exact Connection and must be cleaned up when it specifically ends. */
167
+ onSessionEnd?: (connection: Readonly<Connection>) => void;
168
+ /** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- alongside (never instead of) this session's own internal peerDeviceId resolution, which uses this identical hook internally and keeps working unchanged. See createMeshSession's own onPeerAdvert doc comment for why this exists: a gossip-expansion consumer (wire-mesh#187) needs to observe every advert without becoming a second, competing consumer of this session's own single-reader events stream. */
169
+ onPeerAdvert?: (advert: PeerAdvert) => void;
170
+ }
171
+ /** 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. */
172
+ declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
173
+ //#endregion
174
+ export { createMeshSession as _, DirectoryEntry as a, HANDSHAKE_TIMEOUT_MS as c, IncomingManageRequest as d, ManageOutcome as f, acceptMeshSession as g, SessionEvent as h, DataDomainFrame as i, HandshakeStatus as l, ReconnectPolicy as m, AcceptedMeshSessionOptions as n, FRAME_LOG_WINDOW as o, MeshSession as p, ConnectionState as r, FrameLogEntry as s, AcceptedMeshSession as t, IncomingDataFrame as u };
@@ -0,0 +1,174 @@
1
+ import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "./protocol-Cb3BnuTL.mjs";
2
+ import { r as Transport, t as Connection } from "./transport-CAD419ge.mjs";
3
+ import { r as IdentityPort } from "./identity-CUr-jaAB.mjs";
4
+ import { t as Clock } from "./clock-DhUPRfLh.mjs";
5
+ //#region src/domain/local-fault.d.ts
6
+ /** What `onFault` is told besides the error. */
7
+ interface FaultContext {
8
+ /** The peer the failing hello or send was for, when it is known by then. */
9
+ readonly peer: DeviceId | undefined;
10
+ /** How many reports for this peer were withheld since the last one: the shared fault reporter makes at most one report per peer per interval and counts the rest. */
11
+ readonly suppressed: number;
12
+ }
13
+ /** What the owner of a session hears of failures local to this side: the error, and which peer it was for and how many reports for that peer were withheld. */
14
+ type LocalFaultHandler = (error: unknown, context: FaultContext) => void | Promise<void>;
15
+ //#endregion
16
+ //#region src/domain/mesh-session.d.ts
17
+ /** 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. */
18
+ declare const HANDSHAKE_TIMEOUT_MS = 3000;
19
+ type ConnectionState = {
20
+ status: "idle";
21
+ } | {
22
+ status: "connecting";
23
+ address: string;
24
+ } | {
25
+ status: "connected";
26
+ address: string;
27
+ handshake: HandshakeStatus;
28
+ } | {
29
+ status: "reconnecting";
30
+ address: string;
31
+ attempt: number;
32
+ reason: string;
33
+ } | {
34
+ status: "closed";
35
+ address: string;
36
+ reason: string;
37
+ };
38
+ /** Opt-in retry behaviour for a session's own reconnect attempts. Left at its default `null` in createMeshSession, a disconnect always falls straight to `"closed"` -- today's exact behaviour, unchanged unless a caller opts in. */
39
+ interface ReconnectPolicy {
40
+ maxAttempts: number;
41
+ /** Backoff before attempt N (1-indexed), in milliseconds. */
42
+ delayMs: (attempt: number) => number;
43
+ }
44
+ /** The union `manage-response-frame.outcome` actually carries -- no dedicated generated type name exists for it. */
45
+ type ManageOutcome = ManageOk | ManageError;
46
+ /** One `manage-request` this session received from its peer, surfaced for the caller to act on and answer. */
47
+ interface IncomingManageRequest {
48
+ requestId: number;
49
+ command: ManageCommand;
50
+ scope: CapabilityScope;
51
+ token?: CapabilityToken;
52
+ /** The device-id of the peer this request was relayed on behalf of, as the end-to-end secure channel it arrived through authenticated it (spec/secure-channel.cddl): the identity that channel's handshake proved, never the `from-device` a hub stamps on relay-data, which a hub can set to anything. Present only when the request arrived through a relay pairing. A caller that needs to address a further request back to the same peer (one not sent via respond(), which already routes back correctly on its own) passes this as sendManageRequest's targetDevice. */
53
+ fromDevice?: DeviceId;
54
+ /** The device-id this request's relay-data frame was explicitly addressed to, read from its own `to-device` field -- present only when the request arrived relay-wrapped and the frame carried one. A caller fronting more than one locally-addressable device behind a single hub connection (a gateway advertising several local peers through the same relay pairing) uses this to decide whether the request is for this device or should be routed on to a different local peer it also advertises; this session has no such routing logic of its own, since it represents exactly one identity. */
55
+ toDevice?: DeviceId;
56
+ respond: (outcome: ManageOutcome) => Promise<void>;
57
+ }
58
+ /** The core/data frames: the raw have, request and entries primitive data-sync.ts answers. */
59
+ type DataDomainFrame = DataHaveFrame | DataRequestFrame | DataEntriesFrame;
60
+ /** One core/data frame this session received, surfaced for the application's replication policy to answer. */
61
+ interface IncomingDataFrame {
62
+ frame: DataDomainFrame;
63
+ /** The device-id of the peer that sent this frame through a relay pairing, as the end-to-end secure channel it arrived through authenticated it (spec/secure-channel.cddl), never the `from-device` a hub stamps. Absent for a frame that arrived directly on the connection, whose sender is the connection's own peer. A reply goes back to this device through sendDataFrame's targetDevice. */
64
+ fromDevice?: DeviceId;
65
+ /** The device-id the frame's relay-data was addressed to, for a caller fronting several local devices behind one hub connection, exactly as IncomingManageRequest.toDevice. Present only for a relayed frame that carried one. */
66
+ toDevice?: DeviceId;
67
+ }
68
+ type HandshakeStatus = {
69
+ status: "pending";
70
+ } | {
71
+ status: "negotiated";
72
+ version: ProtocolVersion;
73
+ sharedDomains: string[];
74
+ } | {
75
+ status: "unanswered";
76
+ } | {
77
+ status: "rejected";
78
+ reason: string;
79
+ };
80
+ interface SessionEvent {
81
+ state: ConnectionState;
82
+ /** The peer directory as of this event: latest peer-advert per device-id, in first-heard order. */
83
+ directory: readonly DirectoryEntry[];
84
+ /** The most recent frames that crossed the connection, sent or received, in order: at most FRAME_LOG_WINDOW entries, the recent-activity window a console shows rather than a session history. */
85
+ frameLog: readonly FrameLogEntry[];
86
+ }
87
+ interface DirectoryEntry {
88
+ device: DeviceId;
89
+ advert: PeerAdvert;
90
+ }
91
+ /**
92
+ * How many entries a session's frameLog keeps: the log exists for a console's
93
+ * recent-activity view, not as a session history, so it holds the most recent
94
+ * window of frames rather than every frame a long-lived session ever crossed.
95
+ * Without the bound, a busy session (a relay hub pairing, or any peer on a
96
+ * chatty mesh) retains every frame for its whole life, which on an
97
+ * always-on node grows without limit.
98
+ */
99
+ declare const FRAME_LOG_WINDOW = 256;
100
+ interface FrameLogEntry {
101
+ direction: "sent" | "received";
102
+ frame: Frame;
103
+ }
104
+ interface MeshSession {
105
+ readonly events: AsyncIterable<SessionEvent>;
106
+ /** Every `manage-request` received from the peer, in arrival order. */
107
+ readonly incomingManageRequests: AsyncIterable<IncomingManageRequest>;
108
+ /** Every core/data frame received, direct or through a relay pairing, in arrival order. A relayed one arrives only when it opened under the secure channel of the device that sent it, and names that device in `fromDevice`. What a frame means, and what to answer, is data-sync.ts's and the application's business, so the session only delivers it. */
109
+ readonly incomingDataFrames: AsyncIterable<IncomingDataFrame>;
110
+ /** Every `revocation-entry` received from the peer, in arrival order -- a `revocation-announce` frame's own `entries` array is flattened to one item per entry, since each entry is independently verifiable and independently meaningful regardless of which frame carried it. A consumer typically feeds each one into a RevocationView's own `record`. */
111
+ readonly revocationAnnouncements: AsyncIterable<RevocationEntry>;
112
+ /** Every `coordinator-frame` received from the peer, in arrival order: a gossiped, term-based claim to the rendezvous role (spec/transport.cddl), surfaced raw exactly like revocationAnnouncements because what a claim means is coordinator-election.ts's own business, not the session's. A consumer feeds each one into a CoordinatorElection's evaluate, and sends its own claims with sendCoordinatorClaim. */
113
+ readonly coordinatorFrames: AsyncIterable<CoordinatorFrame>;
114
+ connect: (address: string, localDomains: readonly string[]) => Promise<void>;
115
+ sendPing: () => Promise<void>;
116
+ /**
117
+ * Sends a ping-frame and resolves with the round-trip time in milliseconds once the correlated pong-frame arrives.
118
+ *
119
+ * 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.
120
+ *
121
+ * 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.
122
+ *
123
+ * 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.
124
+ *
125
+ * 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.
126
+ */
127
+ sendPingMeasureRtt: (timeoutMs?: number) => Promise<number>;
128
+ /** Attaches this token to every `manage-request` sent from now on. */
129
+ setToken: (token: CapabilityToken) => void;
130
+ /** Announces one or more already-minted revocation-entries to the peer. Sent directly over the connection, never relay-wrapped -- revocation-announce is a gossiped broadcast, not a request addressed to a specific peer, so it has no targetDevice/token parameters the way sendManageRequest does. */
131
+ sendRevocationAnnounce: (entries: readonly RevocationEntry[]) => Promise<void>;
132
+ /** Sends one coordinator-frame directly over this session's own connection, never relay-wrapped: the claim is a gossiped broadcast like revocation-announce, not a request addressed to a specific peer. The frame itself comes from a CoordinatorElection (claim, or announceCurrent for a refresh), so the session never invents a term on its own. */
133
+ sendCoordinatorClaim: (frame: Readonly<CoordinatorFrame>) => Promise<void>;
134
+ /** Re-sends this side's own self-advert with a fresh snapshot-seconds and, when given, extensions merged onto peer-advert's own open `* tstr => any` tail -- the mechanism a caller uses to keep gossiped presence status (or any other advertised fact) live over a connection's lifetime, since the initial self-advert wireUpConnection sends at connect time is otherwise never repeated. Callers own their own re-advertisement cadence (there is no timer inside MeshSession itself, matching its own DOM-free, fully unit-testable design); a caller not calling this again after connecting is exactly today's existing gossip-once-on-connect behaviour. */
135
+ sendGossipUpdate: (extensions?: Record<string, unknown>) => Promise<void>;
136
+ /** This session's own current topology snapshot -- the identical `topology/peers` value buildSelfAdvert merges into every gossiped self-advert (wire-mesh#180), read back directly rather than only from a possibly-stale gossiped copy elsewhere in the mesh. The live, cache-bust half of the cached-vs-live split `topology.get` itself establishes: a caller answering an incoming topology.get manage-request (see topology.ts's createTopologyGetHandler) calls this to build the response. Synchronous and side-effect-free -- unlike every other method here, it sends nothing and works even before this session has ever connected (an unconnected session simply has no direct peer and no relay pairings yet, both honestly empty). */
137
+ getTopologyPeers: () => TopologyPeers;
138
+ /** Sends one core/data frame (data-have, data-request, or data-entries): the transport half of an application's own noticeboard replication policy (data-sync.ts owns what the frames MEAN; this owns getting one onto the wire), the same layering sendRevocationAnnounce already established for its own frame kind. Without targetDevice it goes directly over this session's own connection, never relay-wrapped, which is how a hub's mailbox is addressed. With targetDevice it is routed to that specific peer through a relay pairing, sealed on the secure channel with that device like a manage-request (spec/secure-channel.cddl), so two devices that reach each other only through a hub can run the data-domain exchange; the hub sees ciphertext and its mailbox is not involved. The peer's own frames come back through incomingDataFrames. Rejects when not connected, exactly like every other send method here. */
139
+ sendDataFrame: (frame: DataDomainFrame, targetDevice?: DeviceId) => Promise<void>;
140
+ /** Sends a manage-request and resolves with the matching manage-response's outcome, correlated by request-id. When targetDevice is given, the request is routed to that specific peer via an established relay-connect pairing (wrapped as relay-data) rather than sent directly over this session's own Connection -- relay-hub deliberately drops manage-request/manage-response frames sent to it directly, since routing between two connected peers is not the relay role's business, so a specific peer reachable only through a relay hub can only be addressed this way. Absent, this sends directly over the Connection exactly as before. When token is given, it is attached to this one request instead of whatever setToken last set -- a single session routinely needs a different token per request when its peer shares more than one scope with this side (e.g. several core/room memberships over one connection), and a session-global token can only ever be correct for one of them. Absent, this request carries setToken's own session-global token exactly as before. When timeoutMs is given, the returned promise resolves with `{ result: "error", code: "timeout" }` rather than hanging forever if no manage-response arrives in time -- a held-open request (a human approval, a not-yet-online peer) otherwise has no way for the caller to give up on it. Absent, this request waits exactly as before, with no time limit of its own. */
141
+ sendManageRequest: (command: ManageCommand, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, token?: CapabilityToken, timeoutMs?: number) => Promise<ManageOutcome>;
142
+ close: () => Promise<void>;
143
+ }
144
+ declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null,
145
+ /** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
146
+ addresses?: readonly string[],
147
+ /** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- the hook a gossip-expansion consumer (wire-mesh#187, gossip-expansion.ts's own createGossipExpansion) uses to observe newly-gossiped peers without becoming a second, competing consumer of this session's own single-reader events stream (each emitted SessionEvent wakes at most one waiter, so a second for-await loop over events would silently steal events from whichever consumer already reads it). Omit for a caller with no use for it, exactly today's behaviour. */
148
+ onPeerAdvert?: (advert: PeerAdvert) => void,
149
+ /** 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. */
150
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
151
+ /** 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. Reports go through the shared fault reporter (fault-report.ts): at most one per peer per its interval, with the number withheld in between given as `suppressed` in the second argument, and at most its budget across all peers per interval, so a failure repeated for one peer is reported once and then counted. If the callback throws, or returns a promise that rejects, the reporter reports that to the console and swallows it. A report held back at the end of an interval is delivered when the interval ends, which can be after the session has closed. Omit for a caller with no use for it. */
152
+ onLocalFault?: LocalFaultHandler): MeshSession;
153
+ /** 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. */
154
+ interface AcceptedMeshSession extends MeshSession {
155
+ /** 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. */
156
+ peerDeviceId: Promise<DeviceId>;
157
+ }
158
+ interface AcceptedMeshSessionOptions {
159
+ /** A caller-chosen label for this connection, used only for ConnectionState's own address field -- the Connection/Transport ports expose no remote-address concept an accepted connection could report on its own (see wire-mesh#45). Defaults to a fixed placeholder since most callers have nothing more specific to offer; a transport adapter that does know the remote's address should pass it here. */
160
+ label?: string;
161
+ clock?: Readonly<Clock>;
162
+ /** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert. Omit (or pass none) for a caller with nothing to offer. */
163
+ addresses?: readonly string[];
164
+ /** Observes every frame this session receives, alongside (never instead of) its own manage-request/gossip/handshake handling (wire-mesh#102) -- the integration point an "ordinary opted-in node relays for peers it's already talking to" uses: pass a shared RelayHub instance's own handleFrame, bound to this connection, and it sees the identical frame stream this session's own applyFrame already consumes, with no second for-await loop racing over the same connection.receive(). A dedicated wire-mesh/cloudflare-hub deployment, which never runs a MeshSession at all, is unaffected -- this is additive, opt-in, and irrelevant to that case. */
165
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>;
166
+ /** Fired once this session's own connection.receive() stream ends -- the counterpart a RelayHub's own forgetConnection needs, since its registry is keyed by this exact Connection and must be cleaned up when it specifically ends. */
167
+ onSessionEnd?: (connection: Readonly<Connection>) => void;
168
+ /** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- alongside (never instead of) this session's own internal peerDeviceId resolution, which uses this identical hook internally and keeps working unchanged. See createMeshSession's own onPeerAdvert doc comment for why this exists: a gossip-expansion consumer (wire-mesh#187) needs to observe every advert without becoming a second, competing consumer of this session's own single-reader events stream. */
169
+ onPeerAdvert?: (advert: PeerAdvert) => void;
170
+ }
171
+ /** 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. */
172
+ declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
173
+ //#endregion
174
+ export { createMeshSession as _, DirectoryEntry as a, HANDSHAKE_TIMEOUT_MS as c, IncomingManageRequest as d, ManageOutcome as f, acceptMeshSession as g, SessionEvent as h, DataDomainFrame as i, HandshakeStatus as l, ReconnectPolicy as m, AcceptedMeshSessionOptions as n, FRAME_LOG_WINDOW as o, MeshSession as p, ConnectionState as r, FrameLogEntry as s, AcceptedMeshSession as t, IncomingDataFrame as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "2.0.0",
@@ -23,7 +23,8 @@
23
23
  "tsdown": "0.23.0",
24
24
  "turbo": "2.10.12",
25
25
  "typescript": "6.0.3",
26
- "vitest": "5.0.0"
26
+ "vitest": "5.0.0",
27
+ "wire-mesh-core-released": "npm:wire-mesh-core@4.1.5"
27
28
  },
28
29
  "exports": {
29
30
  "./adapters/byte-stream-connection": {